NORTH AMERICAN MEDIA EXPERTS // MERIDIAN
LIVE
// LIVE
LOADING DATA...
// AI PERFORMANCE REPORT
LOADING...
⚡ GENERATING REPORT WITH AI...
// FETCHING LIVE ANALYTICS...
// SNAPSHOT: JUL 20, 2026 · APR 19 — JUL 18, 2026 · namediaexperts.com // REFRESH ANYTIME IN COWORK
Total Clicks
70
CLK
Impressions
25.3K
IMP
Avg CTR
0.3%
CTR
Avg Position
21.9
POS
// HIGH IMPRESSIONS · ZERO CLICKS — ACT ON THESE NOW
Google Ads Agency
516 impressions · 0 clicks · avg pos ~22 · optimize title + meta
516
IMPRESSIONS
Google Ads Agency Toronto
309 impressions · 0 clicks · local SEO — add Toronto schema markup
309
IMPRESSIONS
Meta Ads Agency Toronto
/paidsocial has 1,015 impressions — needs CTR optimization urgently
190
IMPRESSIONS
LinkedIn Ads Agency Toronto
152 impressions — no dedicated page exists yet, create one
152
IMPRESSIONS
// TOP PAGES BY CLICKS
PageClicksImpr.
Homepage261,164
Retail Media Benchmarks116,110
CTV Advertising Benchmarks87,546
About5522
Ryan Roberts378
Conversion Rate Benchmarks2448
Microsoft vs Google Ads2427
DSA to AI Max Migration2191
// TOP SEARCH QUERIES
QueryClkImpr
paid media services158
ctv cpm benchmarks143
google ads agency0516
google ads agency toronto0309
media experts0246
google ads mgmt toronto0199
meta ads agency toronto0190
linkedin ads agency toronto0152
// CTV CONTENT IS YOUR BIGGEST SEO ASSET
CTV Benchmarks — 7,546 impressions · 8 clicks0.1% CTR → optimize title
Retail Media Benchmarks — 6,110 impressions · 11 clicks0.18% CTR → A/B test
// SEARCH CONSOLE SNAPSHOT · namediaexperts.com · REFRESH VIA COWORK //
// FETCHING LINKEDIN ADS...
// LIVE HIGHLIGHTS
// AI ASSISTANT
Ask me anything about your metrics...
TAP TO SPEAK
◄ MAIN WEBSITE +Math.round(projected).toLocaleString(); if(paceSub)paceSub.textContent='Projected EOM spend'; } function renderActionItems(d){ const el=document.getElementById('mrdActions'); if(!el)return; const items=[]; if(d.ctr!=null){ if(d.ctr<0.5)items.push({color:'red',text:'CTR is critically low ('+d.ctr.toFixed(2)+'%). Refresh ad creatives and test new headlines immediately.'}); else if(d.ctr<1)items.push({color:'yellow',text:'CTR below benchmark ('+d.ctr.toFixed(2)+'%). A/B test 2-3 new ad variations this week.'}); else items.push({color:'green',text:'CTR is strong ('+d.ctr.toFixed(2)+'%). Scale top-performing ads for increased reach.'}); } if(d.bounceRate!=null){ if(d.bounceRate>70)items.push({color:'red',text:'High bounce rate ('+d.bounceRate.toFixed(1)+'%). Align landing page messaging with ad copy.'}); else if(d.bounceRate>50)items.push({color:'yellow',text:'Bounce rate elevated ('+d.bounceRate.toFixed(1)+'%). Review page load speed and above-fold content.'}); else items.push({color:'green',text:'Bounce rate healthy ('+d.bounceRate.toFixed(1)+'%). Visitors are engaging with content.'}); } if(d.convRate!=null){ if(d.convRate<1)items.push({color:'red',text:'Conversion rate low ('+d.convRate.toFixed(2)+'%). Audit checkout flow and reduce friction points.'}); else if(d.convRate<2)items.push({color:'yellow',text:'Conversion rate moderate ('+d.convRate.toFixed(2)+'%). Test CTA copy and offer presentation.'}); else items.push({color:'green',text:'Conversion rate solid ('+d.convRate.toFixed(2)+'%). Maintain current landing page experience.'}); } if(items.length===0){el.innerHTML='
Smart Action Items
Connect data sources to generate insights.
';return;} el.innerHTML='
Smart Action Items
'+ items.map(i=>'
'+i.text+'
').join(''); } function buildFallback(d){ const parts=[]; if(d.sessions)parts.push(d.sessions.toLocaleString()+' sessions'); if(d.ctr)parts.push(d.ctr.toFixed(2)+'% CTR'); if(d.metaSpend)parts.push('`); win.document.close(); } // ── Session state ───────────────────────────────────────────────────────────── let _aiSpeaking = false; // true while AI voice is playing let _sessionActive = false; // true while a conversation session is open let _shownFollowUp = false; // tracks if "anything else?" has been asked this session let _goodbyeTimer = null; // fires after 5s of silence let _queryAbort = null; // AbortController — cancelled when a new question interrupts async function processVoiceQuery(question) { clearTimeout(_goodbyeTimer); // Cancel any in-flight query immediately if (_queryAbort) { _queryAbort.abort(); } const ctrl = new AbortController(); _queryAbort = ctrl; document.getElementById('vpQuestion').textContent = '"' + question + '"'; document.getElementById('vpQuestion').style.display = 'block'; document.getElementById('vpAnswer').textContent = '// ANALYZING...'; const switched = await detectAndSwitchDateRange(question); if (ctrl.signal.aborted) return; if (switched) document.getElementById('vpAnswer').textContent = '// LOADING DATA...'; let answer; try { const res = await fetch(`${WORKER}/api/ai/ask`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, context: window.aiContext }), signal: ctrl.signal }); if (ctrl.signal.aborted) return; const data = await res.json(); if (data.error || !data.answer) throw new Error(data.error || 'empty'); answer = data.answer; } catch(e) { if (ctrl.signal.aborted) return; // interrupted — stop here answer = localSmartAnswer(question); } if (ctrl.signal.aborted) return; document.getElementById('vpAnswer').textContent = answer; playUISound('answer'); _aiSpeaking = true; if (!voiceMuted) await speak(cleanForSpeech(answer), ctrl.signal); _aiSpeaking = false; if (ctrl.signal.aborted) return; // Ask "anything else?" once per session, after first answer if (!_shownFollowUp && _sessionActive) { _shownFollowUp = true; await new Promise(r => setTimeout(r, 800)); if (ctrl.signal.aborted) return; const fq = 'Is there anything else you need help with?'; document.getElementById('vpAnswer').textContent = fq; document.getElementById('vpQuestion').style.display = 'none'; _aiSpeaking = true; if (!voiceMuted) await speak(fq, ctrl.signal); _aiSpeaking = false; if (ctrl.signal.aborted) return; } // Keep mic open — 5s silence closes the session if (_sessionActive) { setListening(true); _goodbyeTimer = setTimeout(async () => { _sessionActive = false; _shownFollowUp = false; recognition.stop(); setListening(false); const bye = "If you need anything else, I'm one tap away."; document.getElementById('vpAnswer').textContent = bye; document.getElementById('vpQuestion').style.display = 'none'; _aiSpeaking = true; if (!voiceMuted) await speak(bye, null); _aiSpeaking = false; setTimeout(() => { const panel = document.getElementById('voicePanel'); if (panel) panel.classList.remove('show'); }, 3000); }, 5000); } } // ── Date range detection ─────────────────────────────────────────────────────── const DATE_RANGE_PATTERNS = [ { pattern: /last\s+(7|seven)\s+day|past\s+(7|seven)\s+day|last\s+week|past\s+week/i, preset: 'last_7d' }, { pattern: /last\s+(30|thirty)\s+day/i, preset: 'last_30d' }, { pattern: /last\s+(90|ninety)\s+day|last\s+quarter|past\s+quarter/i, preset: 'last_90d' }, { pattern: /this\s+month|current\s+month|month\s+to\s+date|mtd/i, preset: 'this_month' }, { pattern: /last\s+month|previous\s+month/i, preset: 'last_month' }, { pattern: /last\s+year|past\s+year|365/i, preset: 'last_year' }, ]; function wordsToNum(str) { const map = { one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10, eleven:11,twelve:12,thirteen:13,fourteen:14,fifteen:15,sixteen:16,seventeen:17, eighteen:18,nineteen:19,twenty:20,'twenty-one':21,'twenty-two':22,'twenty-three':23, 'twenty-four':24,'twenty-five':25,thirty:30,forty:40,fifty:50,sixty:60, seventy:70,eighty:80,ninety:90,hundred:100 }; const n = parseInt(str); if (!isNaN(n)) return n; const lower = str.toLowerCase().trim(); if (map[lower] !== undefined) return map[lower]; const parts = lower.split(/[\s-]+/); if (parts.length === 2 && map[parts[0]] !== undefined && map[parts[1]] !== undefined) return map[parts[0]] + map[parts[1]]; return null; } // Set an exact custom day range (any number of days) and reload all data function setCustomDaysRange(n) { const fmtDate = d => d.toISOString().split('T')[0]; const end = new Date(); const start = new Date(); start.setDate(start.getDate() - n); window.dateRange = { preset: 'custom', start: fmtDate(start), end: fmtDate(end), label: `LAST ${n} DAYS` }; // Sync the UI picker and show custom inputs with the computed values const picker = document.getElementById('dateRangePicker'); if (picker) picker.value = 'custom'; const cdr = document.getElementById('customDateRange'); if (cdr) cdr.style.display = 'flex'; const si = document.getElementById('customStart'); const ei = document.getElementById('customEnd'); if (si) si.value = fmtDate(start); if (ei) ei.value = fmtDate(end); refreshAll(); } async function switchToPreset(preset) { if (window.dateRange && window.dateRange.preset !== preset) { const picker = document.getElementById('dateRangePicker'); if (picker) { picker.value = preset; onDateRangeChange(preset); } await new Promise(resolve => setTimeout(resolve, 2800)); } } async function detectAndSwitchDateRange(question) { // Check specific named presets first (last week, last month, etc.) for (const { pattern, preset } of DATE_RANGE_PATTERNS) { if (pattern.test(question)) { await switchToPreset(preset); return true; } } // "last/past N days" — exact day count, any number (3, 13, 22, forty-five, etc.) const dayMatch = question.match(/(?:last|past)\s+([\w\s-]+?)\s+days?/i); if (dayMatch) { const n = wordsToNum(dayMatch[1].trim()); if (n && n > 0) { setCustomDaysRange(n); await new Promise(resolve => setTimeout(resolve, 2800)); return true; } } // "last/past N weeks" — converts to exact days const weekMatch = question.match(/(?:last|past)\s+([\w]+)\s+weeks?/i); if (weekMatch) { const n = wordsToNum(weekMatch[1].trim()); if (n && n > 0) { setCustomDaysRange(n * 7); await new Promise(resolve => setTimeout(resolve, 2800)); return true; } } // "last/past N months" — converts to exact days const monthMatch = question.match(/(?:last|past)\s+([\w]+)\s+months?/i); if (monthMatch) { const n = wordsToNum(monthMatch[1].trim()); if (n && n > 0) { setCustomDaysRange(n * 30); await new Promise(resolve => setTimeout(resolve, 2800)); return true; } } return false; } function localSmartAnswer(q) { const lower = q.toLowerCase(); const ctx = window.aiContext || {}; const ga = ctx.ga || {}; const meta = ctx.meta || {}; const li = ctx.linkedin || {}; const vis = ctx.visitors || {}; const period = ga.period || meta.period || 'this period'; const has = (...words) => words.some(w => lower.includes(w)); // ── Traffic performance (broad overview) ───────────────────────────────── if (has('traffic performance','website traffic','site traffic','web traffic', 'traffic report','how is the site','site doing','website doing')) { if (ga.sessions) { let resp = `Website traffic for ${period}: ${Number(ga.sessions).toLocaleString()} sessions from ${Number(ga.users).toLocaleString()} active users, ${Number(ga.views).toLocaleString()} page views, ${ga.pagesPerUser} pages per user, and a ${ga.conversionRate} conversion rate. Your top traffic channel is ${ga.topChannel}.`; if (ga.bounceRate) resp += ` Bounce rate is ${ga.bounceRate}.`; if (ga.avgDuration) resp += ` Average session duration is ${ga.avgDuration}.`; const sessions = Number(ga.sessions); if (sessions < 500) resp += ' Traffic is on the lighter side — consider boosting it with paid search, social ads, or a content push targeting your top keywords.'; else if (sessions >= 500 && sessions < 2000) resp += ' Solid traffic volume. To accelerate growth, consider scaling your best-performing ad channel or publishing more SEO-targeted content.'; else resp += ' Strong traffic volume. Focus on improving conversion rate and reducing bounce rate to maximize the value of this audience.'; return resp; } return 'Switch to the GA4 Analytics tab first so I can load your traffic data, then ask again.'; } // ── Bounce rate ──────────────────────────────────────────────────────────── if (has('bounce','bounce rate')) { if (ga.bounceRate) { const br = parseFloat(ga.bounceRate); const grade = br < 40 ? 'Excellent — visitors are highly engaged.' : br < 55 ? 'Good — within a healthy range.' : br < 70 ? 'This is average — consider improving landing page engagement.' : 'This is high — your landing pages may need optimization.'; const tip = br >= 55 ? ' To bring this down, consider faster page load times, stronger above-the-fold headlines, or clearer calls to action on your landing pages.' : ' To keep engagement strong, continue matching your ad messaging to what visitors find on the landing page.'; return `Your bounce rate is ${ga.bounceRate} for ${period}. ${grade} Industry average is typically 45 to 65 percent.${tip}`; } return 'Switch to the analytics tab first to load your bounce rate data, then ask again.'; } // ── Session duration / time on site ─────────────────────────────────────── if (has('session duration','time on','time spent','how long','average time','dwell','time per')) { if (ga.avgDuration) { const dur = parseFloat(ga.avgDuration); const tip = dur < 60 ? ' To increase time on site, consider adding more engaging content, video, or interactive elements that keep visitors exploring.' : ' Strong session duration — your content is keeping visitors engaged. Consider adding internal links to guide them toward a conversion.'; return `Your average session duration is ${ga.avgDuration} for ${period}. This reflects how long visitors spend on your site per visit.${tip}`; } return 'Switch to the analytics tab first to load session duration data, then ask again.'; } // ── New vs returning visitors ────────────────────────────────────────────── if (has('new user','new visitor','return visitor','returning','repeat visit','loyal','new vs')) { if (ga.newUsersPct) return `${period}: ${ga.newUsersPct} of your visitors are new users and ${ga.returningPct} are returning visitors. You had ${Number(ga.newUsers||0).toLocaleString()} new users out of ${Number(ga.users).toLocaleString()} total.`; return 'Open the GA4 tab to see new vs returning visitor data.'; } // ── Top pages / landing pages ───────────────────────────────────────────── if (has('top page','best page','most visited','popular page','landing page','most traffic page','which page','highest traffic page')) { if (ga.topPages && ga.topPages.length) { const top = ga.topPages.slice(0,3).map((p,i)=>{ const label = p.path==='/'?'Homepage':p.path.split('/').filter(Boolean).pop().replace(/-/g,' ').replace(/\b\w/g,c=>c.toUpperCase()); return `${i+1}. ${label} (${Number(p.views).toLocaleString()} views)`; }).join(', '); return `Your top pages ${period}: ${top}. If these pages have strong traffic but low conversions, consider adding clearer calls to action or a lead capture form above the fold.`; } return 'Open the GA4 tab to see your top performing pages.'; } // ── Geographic / location data ──────────────────────────────────────────── if (has('city','cities','location','geography','where are my visitor','country','state','region','geo','where is traffic coming')) { if (ga.topCities && ga.topCities.length) { const cities = ga.topCities.slice(0,4).map(c=>`${c.city} (${Number(c.users).toLocaleString()})`).join(', '); return `Your top visitor locations ${period} by city: ${cities}.`; } return 'Open the GA4 tab to see geographic breakdown of your visitors.'; } // ── Device breakdown ────────────────────────────────────────────────────── if (has('device','mobile','desktop','tablet','what device','which device','phone','laptop','screen size','mobile traffic','mobile vs')) { if (ga.devices && Object.keys(ga.devices).length) { const sorted = Object.entries(ga.devices).sort((a,b)=>b[1]-a[1]); const parts = sorted.map(([d,pct])=>`${d}: ${pct}%`).join(', '); const topDev = sorted[0]; const tip = topDev && topDev[0]==='mobile' && topDev[1]>50 ? ' Majority mobile — make sure your site is fully mobile-optimized.' : ''; return `Device breakdown ${period}: ${parts}.${tip}`; } return 'Open the GA4 tab to see device breakdown.'; } // ── SEO performance ─────────────────────────────────────────────────────── if (has('seo','search engine optimization','organic search','search ranking','google ranking','keyword','search perform','how is seo','seo doing')) { if (ga.allChannels && ga.allChannels.length) { const organic = ga.allChannels.find(c=>c.channel.toLowerCase().includes('organic search')); if (organic) { const verdict = organic.pct >= 40 ? 'Strong organic presence.' : organic.pct >= 20 ? 'Moderate organic traffic — room to grow.' : 'Low organic traffic — consider investing in content and SEO.'; return `Organic search (SEO) drove ${Number(organic.sessions).toLocaleString()} sessions (${organic.pct}% of all traffic) ${period}. ${verdict}`; } } if (ga.topChannel && ga.topChannel.toLowerCase().includes('organic')) return `Organic Search is your top channel ${period} with ${Number(ga.sessions||0).toLocaleString()} total sessions.`; return 'Open the GA4 tab. I can then analyze your organic search channel performance.'; } // ── ROAS / ROI ──────────────────────────────────────────────────────────── if (has('roas','return on ad','roi','return on invest','ad return')) { if (meta.spend && ga.conversions && parseInt(ga.conversions) > 0) { const spendNum = parseFloat((meta.spend||'$0').replace(/[$,]/g,'')); const cpa = spendNum > 0 ? `$${(spendNum/parseInt(ga.conversions)).toFixed(2)}` : 'N/A'; return `${period}: ${meta.spend} Meta ad spend generated ${Number(ga.conversions).toLocaleString()} conversions — a cost per conversion of ${cpa}. For true ROAS calculation, connect revenue values to your GA4 conversion events.`; } return 'Open both the Meta Ads and GA4 tabs. ROAS requires both spend and conversion data.'; } // ── Cost per conversion / CPL / CPA ────────────────────────────────────── if (has('cost per conversion','cost per lead','cpl','cost per acqui','cost per result','cpa')) { const parts = []; if (meta.spend && ga.conversions && parseInt(ga.conversions) > 0) { const spendNum = parseFloat((meta.spend||'$0').replace(/[$,]/g,'')); if (spendNum > 0) parts.push(`Meta cost per conversion: $${(spendNum/parseInt(ga.conversions)).toFixed(2)}`); } if (li.spend && li.conversions && parseInt(li.conversions) > 0) { const liSpend = parseFloat((li.spend||'$0').replace(/[$,]/g,'')); if (liSpend > 0) parts.push(`LinkedIn cost per conversion: $${(liSpend/parseInt(li.conversions)).toFixed(2)}`); } if (parts.length) return `${parts.join('. ')} ${period}.`; return 'Open both the ads tab and GA4 tab to calculate cost per conversion.'; } // ── Impressions / reach ─────────────────────────────────────────────────── if (has('impression','reach','how many people saw','exposure','how many saw')) { const parts = []; if (meta.impressions) parts.push(`Meta: ${Number(meta.impressions).toLocaleString()} impressions, ${Number(meta.reach||0).toLocaleString()} reach`); if (li.impressions) parts.push(`LinkedIn: ${Number(li.impressions).toLocaleString()} impressions`); if (parts.length) return `Ad impressions ${period} — ${parts.join('. ')}.`; return 'Open the Meta or LinkedIn Ads tabs to see impression and reach data.'; } // ── Ad frequency / fatigue ──────────────────────────────────────────────── if (has('frequency','ad fatigue','showing too many','ad too often','seeing my ad too','repeat ad')) { if (meta.freq) { const f = parseFloat(meta.freq); const warn = f >= 4 ? ' Warning: frequency above 4 usually causes ad fatigue — consider refreshing your creative.' : f >= 3 ? ' Approaching ad fatigue threshold — monitor CTR for signs of decline.' : ' Frequency is healthy.'; return `Your Meta ad frequency is ${meta.freq} ${period} — each person saw your ads an average of ${meta.freq} times.${warn}`; } return 'Open the Meta Ads tab to see frequency data.'; } // ── CPM ─────────────────────────────────────────────────────────────────── if (has('cpm','cost per thousand','cost per mille','cost per impression')) { const parts = []; if (meta.cpm) parts.push(`Meta CPM is ${meta.cpm}`); if (li.cpm) parts.push(`LinkedIn CPM is ${li.cpm}`); if (parts.length) return `${parts.join('. ')} ${period}. This is the cost per 1,000 ad impressions.`; return 'Open the Meta or LinkedIn Ads tabs to see CPM data.'; } // ── Campaign performance ────────────────────────────────────────────────── if (has('campaign','best campaign','top campaign','which ad is','best ad','ad perform','which campaign')) { if (meta.spend) return `Meta campaigns ${period}: ${meta.spend} total spend, ${Number(meta.impressions||0).toLocaleString()} impressions, ${Number(meta.clicks||0).toLocaleString()} clicks at ${meta.ctr} CTR. Open the Meta Ads tab for the full campaign-by-campaign breakdown.`; return 'Open the Meta Ads tab to see campaign performance details.'; } // ── Daily spend rate ────────────────────────────────────────────────────── if (has('per day','daily spend','daily budget','daily average','spending per day','spend per day')) { if (meta.spend) { const dr = window.dateRange || {}; let days = 30; if (dr.preset === 'last_7d') days = 7; else if (dr.preset === 'last_90d') days = 90; else if (dr.preset === 'last_year') days = 365; else if (dr.preset === 'custom' && dr.start && dr.end) { days = Math.max(1, Math.round((new Date(dr.end) - new Date(dr.start)) / 86400000)); } const spendNum = parseFloat((meta.spend||'$0').replace(/[$,]/g,'')); return `Your average daily Meta ad spend ${period} is $${(spendNum/days).toFixed(2)} (${meta.spend} total over ~${days} days).`; } return 'Open the Meta Ads tab to calculate daily spend.'; } // ── Traffic diagnosis ───────────────────────────────────────────────────── if (has('why is traffic','why did traffic','traffic drop','traffic down','traffic declin','traffic fell','traffic slow','why less','why fewer')) { if (ga.sessions && ga.allChannels) { const top2 = ga.allChannels.slice(0,2).map(c=>`${c.channel} (${c.pct}%)`).join(' and '); return `Current data shows ${Number(ga.sessions).toLocaleString()} sessions ${period} driven primarily by ${top2}. Traffic drops typically stem from: reduced organic rankings, lower ad spend, seasonality, or a technical issue like a broken page. Check the trend chart on the GA4 tab to pinpoint exactly when the drop started.`; } return 'Open the GA4 tab. The session trend chart will show you exactly when traffic changed.'; } // ── Strategy / recommendations ──────────────────────────────────────────── if (has('should i','recommend','what should i','strategy','advice','suggest','what would you','increase budget','increase spend','optimize','improve')) { const advice = []; if (meta.ctr) { const ctr=parseFloat(meta.ctr); if(!isNaN(ctr)&&ctr<1) advice.push('your Meta CTR is below 1% — refresh your ad creative before increasing budget'); else if(!isNaN(ctr)&&ctr>=2) advice.push('your Meta CTR is strong — scaling budget could increase results proportionally'); } if (ga.bounceRate) { const br=parseFloat(ga.bounceRate); if(!isNaN(br)&&br>65) advice.push('your bounce rate is high — optimizing your landing page could significantly improve conversions'); } if (ga.conversionRate) { const cr=parseFloat(ga.conversionRate); if(!isNaN(cr)&&cr<1) advice.push('your conversion rate is under 1% — A/B testing your call-to-action and form placement is recommended'); } if (ga.devices && ga.devices.mobile > 55 && ga.avgDuration) { const d=ga.avgDuration; if(d.includes('s')&&!d.includes('m')&&parseInt(d)<60) advice.push('most traffic is mobile but session duration is short — check your mobile page speed'); } if (advice.length) return `Based on your data ${period}: ${advice.join('; ')}. Always test one change at a time to measure impact.`; return 'Load your GA4 and Meta data first. Then I can give you data-driven recommendations based on your specific numbers.'; } // ── Period comparison ───────────────────────────────────────────────────── if (has('compare','vs last','versus last','this month vs','this week vs','last month vs','month over month','week over week','compare period','how does this')) { return `To compare periods, use the date range picker at the top of the dashboard — switch between Last 7 Days, Last 30 Days, and Last 90 Days and note the numbers. For side-by-side comparison, I recommend asking me for a summary after each date range selection.`; } // ── Channel breakdown detail ─────────────────────────────────────────────── if (has('channel breakdown','all channel','traffic breakdown','all source','where is my traffic','where is all my traffic','traffic coming from','traffic by channel','traffic source')) { if (ga.allChannels && ga.allChannels.length) { const breakdown = ga.allChannels.slice(0,6).map(c=>`${c.channel}: ${c.pct}%`).join(', '); return `Traffic channel breakdown ${period}: ${breakdown}. Total sessions: ${Number(ga.sessions||0).toLocaleString()}.`; } return 'Open the GA4 tab to see the full channel breakdown.'; } // ── Sessions / visits ───────────────────────────────────────────────────── if (has('session','visit','how many people','how much traffic')) { if (ga.sessions) return `You had ${Number(ga.sessions).toLocaleString()} sessions and ${Number(ga.users).toLocaleString()} active users ${period}. Top channel: ${ga.topChannel || 'Direct'}. Bounce rate: ${ga.bounceRate || 'N/A'}. Avg session: ${ga.avgDuration || 'N/A'}.`; return 'GA4 data is still loading. Switch to the GA4 tab and wait a moment.'; } // ── Users ───────────────────────────────────────────────────────────────── if (has('user','how many visitor','active user')) { if (ga.users) return `You had ${Number(ga.users).toLocaleString()} active users ${period}. ${ga.newUsersPct ? `${ga.newUsersPct} were new and ${ga.returningPct} returning.` : ''}`; return 'Open the GA4 tab to see user data.'; } // ── Page views ──────────────────────────────────────────────────────────── if (has('page view','pageview','pages per')) { if (ga.views) return `You recorded ${Number(ga.views).toLocaleString()} page views ${period}, averaging ${ga.pagesPerUser} pages per user.`; return 'Switch to the GA4 tab to load page view data.'; } // ── Conversion details — what were the actual conversions? ──────────────── if (has('what were','which conversion','what type of conversion','what kind of conversion', 'what were the conversion','tell me about the conversion','conversion detail', 'what converted','what did convert','which page converted','conversion breakdown', 'what lead','what form','what signup','what purchase','what type of lead')) { if (ga.conversionDetails && ga.conversionDetails.length) { const details = ga.conversionDetails.slice(0,5).map(d=>{ const c = d.count > 1 ? `${d.count}x` : ''; return `${d.eventLabel}${c ? ' ('+c+')' : ''} on ${d.pageLabel}`; }).join('; '); const total = ga.conversionDetails.reduce((s,d)=>s+d.count,0); return `Here are your ${total} conversion${total!==1?'s':''} ${period}: ${details}.`; } if (ga.conversions !== undefined && parseInt(ga.conversions) === 0) return `You had no conversions ${period}. Check that your GA4 conversion events are configured correctly.`; return 'Open the GA4 tab first so I can load your conversion event details.'; } // ── Which pages are converting? ─────────────────────────────────────────── if (has('which page convert','what page convert','page that convert','best converting page','highest converting')) { if (ga.conversionDetails && ga.conversionDetails.length) { const byPage = {}; ga.conversionDetails.forEach(d=>{ byPage[d.pageLabel]=(byPage[d.pageLabel]||0)+d.count; }); const sorted = Object.entries(byPage).sort((a,b)=>b[1]-a[1]); const top = sorted.slice(0,3).map(([page,cnt])=>`${page} (${cnt})`).join(', '); return `Your converting pages ${period}: ${top}.`; } return 'Open the GA4 tab to load conversion page data.'; } // ── Conversions ─────────────────────────────────────────────────────────── if (has('conversion','convert','lead','goal')) { if (ga.conversions !== undefined) { let resp = `You had ${Number(ga.conversions).toLocaleString()} conversion${ga.conversions!=1?'s':''} with a ${ga.conversionRate} conversion rate ${period}.`; if (ga.conversionDetails && ga.conversionDetails.length) { const top = ga.conversionDetails[0]; resp += ` Top conversion event: ${top.eventLabel} on ${top.pageLabel}.`; } const cvr = parseFloat(ga.conversionRate); if (!isNaN(cvr)) { if (cvr < 1) resp += ' A conversion rate below 1% typically signals a landing page or messaging issue — consider A/B testing your headline, CTA button, or offer.'; else if (cvr < 3) resp += ' Your conversion rate is solid. To push it higher, try adding social proof, tightening your call to action, or reducing friction in your form.'; else resp += ' That is an excellent conversion rate. To scale results further, consider increasing ad spend on your top traffic sources.'; } return resp; } return 'No conversion data loaded yet. Open the GA4 tab first.'; } // ── Channels / sources ──────────────────────────────────────────────────── if (has('channel','source','organic','paid','social','email','direct','referral')) { if (ga.topChannel) return `Your top traffic channel is ${ga.topChannel} ${period}. Total sessions: ${Number(ga.sessions||0).toLocaleString()}.`; return 'Open the GA4 tab to see channel breakdown.'; } // ── Meta ads ────────────────────────────────────────────────────────────── if (has('meta','facebook','fb','instagram')) { if (meta.spend) return `Meta Ads ${period}: ${meta.spend} spent, ${Number(meta.impressions||0).toLocaleString()} impressions, ${Number(meta.clicks||0).toLocaleString()} clicks, ${meta.ctr} CTR, ${meta.cpc} CPC. Frequency: ${meta.freq || 'N/A'}.`; return 'Open the Meta Ads tab to load your Facebook and Instagram data.'; } // ── LinkedIn ────────────────────────────────────────────────────────────── if (has('linkedin','linked in')) { if (li.spend) return `LinkedIn Ads ${period}: ${li.spend} spent, ${Number(li.impressions||0).toLocaleString()} impressions, ${li.ctr} CTR, ${li.conversions} conversions.`; return 'LinkedIn Ads data is pending API approval. Check back soon.'; } // ── Spend / budget ──────────────────────────────────────────────────────── if (has('spend','budget','money','dollar','how much','paid')) { const parts = []; if (meta.spend) parts.push(`Meta: ${meta.spend}`); if (li.spend) parts.push(`LinkedIn: ${li.spend}`); if (parts.length) return `Ad spend ${period} — ${parts.join(', ')}.`; return 'Open the Meta or LinkedIn Ads tabs to load spend data.'; } // ── CTR ─────────────────────────────────────────────────────────────────── if (has('ctr','click through','click-through')) { const parts = []; if (meta.ctr) parts.push(`Meta CTR: ${meta.ctr}`); if (li.ctr) parts.push(`LinkedIn CTR: ${li.ctr}`); if (parts.length) { let resp = `${parts.join('. ')} for ${period}.`; const ctrVal = meta.ctr ? parseFloat(meta.ctr) : null; if (ctrVal !== null && !isNaN(ctrVal)) { if (ctrVal < 1) resp += ' A CTR below 1% on Meta usually means the creative or audience needs work — try refreshing the ad visuals or testing a new hook in the first 3 seconds.'; else if (ctrVal < 2) resp += ' Decent CTR. To push it higher, test new ad creatives and make sure your headline speaks directly to your target audience pain point.'; else resp += ' Strong CTR. Your ads are resonating well — now focus on making sure the landing page converts that interest into leads.'; } return resp; } return 'Load the Meta or LinkedIn Ads tabs to see CTR data.'; } // ── CPC ─────────────────────────────────────────────────────────────────── if (has('cpc','cost per click')) { const parts = []; if (meta.cpc) parts.push(`Meta CPC: ${meta.cpc}`); if (li.cpc) parts.push(`LinkedIn CPC: ${li.cpc}`); if (parts.length) return `${parts.join('. ')} ${period}.`; return 'Load the Meta or LinkedIn Ads tabs to see cost per click.'; } // ── Visitors / company identification ───────────────────────────────────── if (has('visitor intel','who is visiting','identify visitor','rb2b','company visit')) { if (vis.total) return `Visitor Intel has identified ${vis.total} companies visiting your site, with ${vis.today} visits today and ${vis.people} people tracked.`; return 'Open the Visitor Intel tab to see which companies are visiting your site.'; } // ── General performance / summary ───────────────────────────────────────── if (has('perform','summary','overview','how are we doing','how is everything','report','everything','all metrics')) { const parts = []; if (ga.sessions) parts.push(`GA4: ${Number(ga.sessions).toLocaleString()} sessions, ${ga.conversionRate} conversion rate, ${ga.bounceRate||'N/A'} bounce rate`); if (meta.spend) parts.push(`Meta Ads: ${meta.spend} spend, ${meta.ctr} CTR`); if (li.spend) parts.push(`LinkedIn: ${li.spend} spend`); if (parts.length) return `Performance summary ${period}: ${parts.join(' | ')}.`; return 'Open each tab to load data, then ask for a summary.'; } // ── Help ────────────────────────────────────────────────────────────────── if (has('help','what can you','what do you','capable','what questions')) { return 'I can answer questions about: website traffic, sessions, bounce rate, session duration, new vs returning visitors, top pages, devices, city data, SEO performance, Meta and LinkedIn ad spend, CTR, CPC, CPM, ROAS, cost per lead, ad frequency, campaign performance, traffic diagnosis, and budget strategy. Try: "What is my bounce rate?" or "Show me device breakdown" or "Why is my traffic down?"'; } // ── General fallback ────────────────────────────────────────────────────── const parts = []; if (ga.sessions) parts.push(`${Number(ga.sessions).toLocaleString()} sessions, ${ga.conversionRate} conversion rate, ${ga.bounceRate||''} bounce rate`); if (meta.spend) parts.push(`Meta: ${meta.spend} spend, ${meta.ctr} CTR`); if (li.spend) parts.push(`LinkedIn: ${li.spend} spend`); if (parts.length) return `Here is your performance summary for ${period}: ${parts.join('. ')}.`; return `I can help with website traffic, bounce rate, session duration, top pages, device breakdown, ad spend, conversions, and more. Make sure the relevant tab is loaded first so I have the data to pull from.`; } // ── Clean text before speaking (strips UI decorators, adds friendly OUTRO) ────── function cleanForSpeech(text) { return text .replace(/\/\//g, '') // remove // decorators .replace(/\bMTD:\s*/gi, 'month to date, ') // MTD → readable .replace(/\s*→\s*/g, ' to ') // → arrows .replace(/\|/g, ',') // pipe separators .replace(/\bTry asking about[^.!?]*/gi, '') // remove "Try asking about..." .replace(/\bFeel free to ask[^.!?]*/gi, '') // remove "Feel free to..." .replace(/\bMake sure the relevant tab[^.!?]*/gi, '') .replace(/\bOpen the GA4 tab\b/gi, 'Open the analytics tab') .replace(/\s{2,}/g, ' ') .trim() ; } // ── Voice: ElevenLabs (realistic) with browser TTS fallback ─────────────────── // ── Audio context (bypasses autoplay restrictions after long async chains) ── let _audioCtx = null; function getAudioCtx() { if (!_audioCtx || _audioCtx.state === 'closed') { _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (_audioCtx.state === 'suspended') _audioCtx.resume(); return _audioCtx; } // Unlock AudioContext on every user interaction document.addEventListener('click', () => { try { getAudioCtx().resume(); } catch(e) {} }, true); // ── UI SOUND ENGINE ─────────────────────────────────────────────────────────── function playUISound(type) { if (voiceMuted) return; try { const ctx = getAudioCtx(); if (type === 'activate') { // Rising 3-note activate when orb is tapped const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(440, ctx.currentTime); osc.frequency.linearRampToValueAtTime(880, ctx.currentTime + 0.12); osc.frequency.linearRampToValueAtTime(1320, ctx.currentTime + 0.22); gain.gain.setValueAtTime(0, ctx.currentTime); gain.gain.linearRampToValueAtTime(0.08, ctx.currentTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35); osc.start(); osc.stop(ctx.currentTime + 0.35); } else if (type === 'deactivate') { // Falling deactivate when orb is tapped to stop const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1320, ctx.currentTime); osc.frequency.linearRampToValueAtTime(440, ctx.currentTime + 0.2); gain.gain.setValueAtTime(0.06, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3); osc.start(); osc.stop(ctx.currentTime + 0.3); } else if (type === 'answer') { // Soft two-note chime when AI finishes answering [0, 0.18].forEach((delay, i) => { const o = ctx.createOscillator(); const g = ctx.createGain(); o.connect(g); g.connect(ctx.destination); o.type = 'sine'; o.frequency.setValueAtTime(i === 0 ? 660 : 880, ctx.currentTime + delay); g.gain.setValueAtTime(0.05, ctx.currentTime + delay); g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + delay + 0.45); o.start(ctx.currentTime + delay); o.stop(ctx.currentTime + delay + 0.45); }); } else if (type === 'nav') { // Subtle tick when clicking sidebar nav const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1100, ctx.currentTime); osc.frequency.linearRampToValueAtTime(800, ctx.currentTime + 0.06); gain.gain.setValueAtTime(0.045, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1); osc.start(); osc.stop(ctx.currentTime + 0.1); } else if (type === 'hover') { // Ultra-subtle hover blip const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1800, ctx.currentTime); gain.gain.setValueAtTime(0.018, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05); osc.start(); osc.stop(ctx.currentTime + 0.05); } } catch(e) {} } // Wire hover + click sounds to interactive elements after DOM ready function initInteractionSounds() { document.querySelectorAll('.sb-nav-item').forEach(el => { if (!el) return; el.addEventListener('mouseenter', () => playUISound('hover')); el.addEventListener('click', () => playUISound('nav')); }); document.querySelectorAll('.mute-hdr-btn, .sb-report-btn, .dr-preset-btn, .dr-apply-btn, .vp-close-btn, .report-dl-btn, .voice-type-send').forEach(el => { if (!el) return; el.addEventListener('mouseenter', () => playUISound('hover')); }); } // Run after full page load to ensure all elements exist window.addEventListener('load', initInteractionSounds); function toggleMute() { voiceMuted = !voiceMuted; const lbl = document.getElementById('muteLabel'); const btn = document.getElementById('muteHdrBtn'); if (voiceMuted) { if (currentAudio) { try { currentAudio.pause(); } catch(e) {} currentAudio = null; } if (window.speechSynthesis) window.speechSynthesis.cancel(); if (lbl) lbl.textContent = 'UNMUTE'; if (lbl) lbl.textContent = 'SOUND OFF'; if (btn) btn.style.opacity = '0.4'; } else { if (lbl) lbl.textContent = 'MUTE'; if (btn) btn.style.opacity = '1'; } } function closeVoicePanel() { // Stop all audio immediately if (currentAudio) { try { currentAudio.pause(); } catch(e) {} currentAudio = null; } if (window.speechSynthesis) window.speechSynthesis.cancel(); // Abort any in-flight query if (_queryAbort) { _queryAbort.abort(); _queryAbort = null; } _aiSpeaking = false; _sessionActive = false; _shownFollowUp = false; clearTimeout(_goodbyeTimer); _goodbyeTimer = null; if (recognition) try { recognition.stop(); } catch(e) {} setListening(false); document.getElementById('voicePanel').classList.remove('show'); } // speak() accepts an optional AbortSignal so it can be cancelled mid-playback async function speak(text, signal) { if (voiceMuted || !text) return; if (signal?.aborted) return; try { const res = await fetch(`${WORKER}/api/tts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), signal }); if (signal?.aborted) return; if (res.ok) { const arrayBuf = await res.arrayBuffer(); if (signal?.aborted) return; const ctx = getAudioCtx(); if (ctx.state === 'suspended') await ctx.resume(); if (signal?.aborted) return; const decoded = await ctx.decodeAudioData(arrayBuf); const src = ctx.createBufferSource(); src.buffer = decoded; src.connect(ctx.destination); currentAudio = { pause: () => { try { src.stop(); } catch(e) {} } }; await new Promise(resolve => { // Resolve immediately if aborted before playback starts if (signal) signal.addEventListener('abort', () => { try { src.stop(); } catch(e) {} currentAudio = null; resolve(); }, { once: true }); src.onended = () => { currentAudio = null; resolve(); }; src.start(0); }); return; } } catch(e) { if (signal?.aborted) return; console.warn('TTS error:', e); } if (signal?.aborted) return; await speakBrowser(text, signal); } function getBestVoice() { const voices = window.speechSynthesis.getVoices(); if (!voices.length) return null; const priority = [ 'Ava (Enhanced)', 'Ava (Premium)', 'Ava', 'Allison (Enhanced)', 'Allison (Premium)', 'Allison', 'Samantha (Enhanced)', 'Samantha (Premium)', 'Susan (Enhanced)', 'Karen (Enhanced)', 'Karen', 'Victoria (Enhanced)', 'Moira', 'Google US English', 'Microsoft Aria Online (Natural) - English (United States)', 'Microsoft Zira - English (United States)', 'Samantha' ]; for (const name of priority) { const v = voices.find(v => v.name === name); if (v) return v; } const enhanced = voices.find(v => v.lang.startsWith('en') && /enhanced|premium/i.test(v.name)); if (enhanced) return enhanced; return voices.find(v => v.lang === 'en-US') || voices.find(v => v.lang.startsWith('en')); } function speakBrowser(text, signal) { if (!window.speechSynthesis) return Promise.resolve(); if (signal?.aborted) return Promise.resolve(); window.speechSynthesis.cancel(); const utt = new SpeechSynthesisUtterance(text); utt.rate = 0.91; utt.pitch = 1.0; utt.volume = 1.0; return new Promise(resolve => { utt.onend = resolve; utt.onerror = resolve; // If aborted before 80ms timer fires, cancel and resolve immediately if (signal) signal.addEventListener('abort', () => { window.speechSynthesis.cancel(); resolve(); }, { once: true }); const trySpeak = () => { if (signal?.aborted) { resolve(); return; } // aborted during the 80ms window const voice = getBestVoice(); if (voice) utt.voice = voice; window.speechSynthesis.speak(utt); }; if (window.speechSynthesis.getVoices().length) { setTimeout(trySpeak, 80); } else { window.speechSynthesis.addEventListener('voiceschanged', () => setTimeout(trySpeak, 80), { once: true }); } }); } // Pre-load voices async if (window.speechSynthesis) { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = () => window.speechSynthesis.getVoices(); } // ── VISITOR INTELLIGENCE ──────────────────────────────────────────────────────── function renderVisitorCard(v) { const div = document.createElement('div'); div.className = 'vis-card'; div.innerHTML = `
${v.company}
${v.industry}
${v.person}
${v.title}
${v.linkedIn ? `🔗 View LinkedIn Profile` : ''}
${v.size || ''}
`; return div; } async function loadVisitors() { if (visitorsLoaded) return; visitorsLoaded = true; const s = (id, v) => { const e = document.getElementById(id); if(e) e.style.display = v; }; s('visLoading', 'none'); s('visLive', 'none'); const showLive = (visitors, errorMsg) => { const grid = document.getElementById('visGrid'); if (!grid) return; grid.innerHTML = ''; const today = new Date().toDateString(); const todayCount = visitors.filter(v => new Date(v.when).toDateString() === today).length; const uniqueCompanies = new Set(visitors.map(v => v.company)).size; const peopleCount = visitors.filter(v => v.person).length; const kpiTotal = document.getElementById('vKpiTotal'); const kpiToday = document.getElementById('vKpiToday'); const kpiPeople = document.getElementById('vKpiPeople'); if (kpiTotal) kpiTotal.textContent = uniqueCompanies; if (kpiToday) kpiToday.textContent = todayCount; if (kpiPeople) kpiPeople.textContent = peopleCount; if (visitors.length) { visitors.forEach(v => { const relTime = formatRelTime(v.when); grid.appendChild(renderVisitorCard({ ...v, when: relTime })); }); } else { grid.innerHTML = `
${errorMsg || '// NO VISITORS IDENTIFIED YET — PIXEL IS ACTIVE AND WARMING UP'}
`; } const ts = new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',timeZoneName:'short'}); const ftr = document.getElementById('visFtr'); if (ftr) ftr.textContent = `// VISITOR DATA · LAST UPDATED ${ts} //`; s('visLive', ''); const badge = document.getElementById('visBadge'); const badgeSoon = document.getElementById('visBadgeSoon'); if (badge) badge.style.display = visitors.length ? '' : 'none'; if (badgeSoon) badgeSoon.style.display = visitors.length ? 'none' : ''; window.aiContext.visitors = { total: uniqueCompanies, today: todayCount, people: peopleCount }; }; try { const res = await fetch(`${WORKER}/api/visitors`); const data = await res.json(); const visitors = data.visitors || []; showLive(visitors); } catch(e) { visitorsLoaded = false; // allow retry on next tab click showLive([], '// COULD NOT REACH VISITOR DATA — CHECK WORKER DEPLOYMENT'); } } // ── SIDEBAR COLLAPSE TOGGLE ─────────────────────────────────────────────────── function toggleSbSection(bodyId, chevronId) { const body = document.getElementById(bodyId); const chev = document.getElementById(chevronId); if (!body) return; const collapsed = body.style.display === 'none'; body.style.display = collapsed ? '' : 'none'; if (chev) chev.style.transform = collapsed ? '' : 'rotate(-90deg)'; } function formatRelTime(iso) { if (!iso) return 'recently'; // Already a formatted string (e.g. "2 min ago") — return as-is if (typeof iso === 'string' && /ago|just now/i.test(iso)) return iso; const d = new Date(iso); if (isNaN(d.getTime())) return 'recently'; const diff = Math.floor((Date.now() - d) / 1000); if (diff < 60) return diff + ' sec ago'; if (diff < 3600) return Math.floor(diff/60) + ' min ago'; if (diff < 86400) return Math.floor(diff/3600) + ' hr ago'; return Math.floor(diff/86400) + ' day ago'; } // ── AI RECOMMENDATIONS + HIGHLIGHTS ────────────────────────────────────────── function updateInsights() { const ga = window.aiContext?.ga || {}; const meta = window.aiContext?.meta || {}; const recs = []; if (ga.bounceRate) { const br = parseFloat(ga.bounceRate); if (br > 70) recs.push({ t:'rw', i:'⚠', h:'High Bounce Rate', d:`${ga.bounceRate} bounce rate — visitors are leaving quickly. A/B test your hero headline and make sure your primary CTA is visible above the fold.` }); else if (br < 40) recs.push({ t:'ro', i:'✓', h:'Strong Page Engagement', d:`${ga.bounceRate} bounce rate — excellent. Visitors are exploring multiple pages. Double down on the content bringing them in.` }); } if (ga.conversionRate) { const cr = parseFloat(ga.conversionRate); if (cr < 1) recs.push({ t:'rw', i:'⚠', h:'Low Conversion Rate', d:`${ga.conversionRate} conversion rate. Simplify your form, add trust signals (testimonials, logos), and test your CTA button copy and color.` }); else if (cr >= 3) recs.push({ t:'ro', i:'✓', h:'Healthy Conversion Rate', d:`${ga.conversionRate} conversion rate — strong. Focus on scaling traffic while maintaining this efficiency.` }); } if (ga.newUsersPct && parseInt(ga.newUsersPct) > 90) { recs.push({ t:'ri', i:'ℹ', h:'Almost All New Visitors', d:`${ga.newUsersPct} new visitors. Launch a retargeting campaign or email capture popup to re-engage first-time visitors and build a returning audience.` }); } if (ga.avgDuration) { const secs = parseInt(ga.avgDuration); const isShort = ga.avgDuration.includes('s') && !ga.avgDuration.includes('m') && secs < 45; if (isShort) recs.push({ t:'rw', i:'⚠', h:'Short Session Duration', d:`Average ${ga.avgDuration} on site. Add an engaging video, interactive tool, or stronger opening hook to increase time-on-page.` }); } if (ga.allChannels) { const direct = ga.allChannels.find(c => c.channel.toLowerCase() === 'direct'); if (direct && direct.pct > 55) recs.push({ t:'ri', i:'ℹ', h:'Heavy Direct Traffic', d:`${direct.pct}% Direct traffic means people already know you. Invest in SEO and paid campaigns to bring in net-new audiences.` }); const ai = ga.allChannels.find(c => c.channel.toLowerCase().includes('ai')); if (ai && ai.pct > 1) recs.push({ t:'ro', i:'★', h:'AI Assistants Sending Traffic', d:`${ai.pct}% of sessions come from AI tools (ChatGPT, Perplexity, etc.). Structure your content to be cited by AI — authoritative, direct, well-headlined.` }); } if (meta.ctr) { const ctr = parseFloat(meta.ctr); if (ctr < 1) recs.push({ t:'rw', i:'⚠', h:'Meta CTR Below 1%', d:`${meta.ctr} CTR on Meta ads. Refresh your creative — try video, a bold new headline, or tighten your audience targeting to improve ad relevance.` }); else if (ctr > 2) recs.push({ t:'ro', i:'✓', h:'Strong Meta CTR', d:`${meta.ctr} CTR — above average. Scale budget on this creative while it's hot, and watch frequency to prevent fatigue.` }); } if (meta.freq && parseFloat(meta.freq) > 3.5) { recs.push({ t:'rw', i:'⚠', h:'Ad Fatigue Risk', d:`Meta frequency is ${meta.freq}×. Your audience is seeing the same ad too often. Introduce new creative variants now before CTR drops.` }); } if (ga.devices) { if ((ga.devices.mobile || 0) > 60) recs.push({ t:'ri', i:'📱', h:'Majority Mobile Traffic', d:`${ga.devices.mobile}% mobile visitors. Audit your mobile page speed (target under 3s) and ensure CTAs are thumb-friendly above the fold.` }); if ((ga.devices.desktop || 0) > 90) recs.push({ t:'ri', i:'🖥', h:'Near-100% Desktop', d:`${ga.devices.desktop}% desktop traffic. Verify your landing pages and ads are optimized for widescreen layouts and desktop user intent.` }); } if (ga.conversions !== undefined && parseInt(ga.conversions) === 0 && parseInt(ga.sessions) > 100) { recs.push({ t:'rw', i:'🚨', h:'Zero Conversions Tracked', d:`No conversions recorded with ${Number(ga.sessions).toLocaleString()} sessions. Check GA4 Admin → Events to confirm your conversion events are marked and firing correctly.` }); } if (recs.length === 0 && (ga.sessions || meta.spend)) { recs.push({ t:'ro', i:'✓', h:'Metrics Look Stable', d:'No critical issues detected. Keep monitoring bounce rate, conversion rate, and ad frequency — small changes can signal larger trends early.' }); } const list = document.getElementById('aiRecsList'); if (list && recs.length) { list.innerHTML = recs.slice(0,5).map(r => `
${r.i}
${r.h}
${r.d}
` ).join(''); } // Highlights ticker const items = []; if (ga.sessions) items.push(`SESSIONS ${Number(ga.sessions).toLocaleString()}`); if (ga.bounceRate) items.push(`BOUNCE ${ga.bounceRate}`); if (ga.conversionRate) items.push(`CVR ${ga.conversionRate}`); if (ga.avgDuration) items.push(`AVG SESSION ${ga.avgDuration}`); if (ga.topChannel) items.push(`TOP CHANNEL ${ga.topChannel.toUpperCase()}`); if (ga.newUsersPct) items.push(`NEW VISITORS ${ga.newUsersPct}`); if (meta.spend) items.push(`META SPEND ${meta.spend}`); if (meta.ctr) items.push(`META CTR ${meta.ctr}`); if (ga.devices) { const top = Object.entries(ga.devices).sort((a,b)=>b[1]-a[1])[0]; if (top) items.push(`TOP DEVICE ${top[0].toUpperCase()} ${top[1]}%`); } if (items.length) { const bar = document.getElementById('highlightsBar'); const inner = document.getElementById('hlTickerInner'); if (bar && inner) { const key = items.join('|'); // Only replace innerHTML when content actually changes — prevents animation restart glitch if (inner.dataset.key !== key) { inner.dataset.key = key; inner.innerHTML = [...items,...items].map(h=>`${h}`).join(''); } bar.style.display = 'flex'; } } } function toggleAIRecs() { const panel = document.getElementById('aiRecsPanel'); const chevron = document.getElementById('aiRecsChevron'); if (!panel) return; const open = panel.style.display === 'block'; panel.style.display = open ? 'none' : 'block'; if (chevron) chevron.style.transform = open ? '' : 'rotate(180deg)'; } function updateBottomVisitors(visitors) { const el = document.getElementById('bottomVisList'); if (!el) return; if (!visitors || !visitors.length) { el.innerHTML = '
// NO VISITORS IDENTIFIED YET — RB2B WARMING UP
'; return; } const today = new Date().toDateString(); el.innerHTML = visitors.slice(0, 8).map(v => { const isToday = v.when && new Date(v.when).toDateString() === today; const relTime = v.when ? formatRelTime(v.when) : ''; const pageLbl = v.page ? (v.page === '/' ? 'Homepage' : v.page.split('/').filter(Boolean).pop().replace(/-/g,' ').slice(0,30)) : ''; return `
${v.company || 'Unknown Company'}
${pageLbl}
${relTime}
`; }).join(''); } // ── GOAL TRACKING ────────────────────────────────────────────────────────────── // Monthly targets for N.A.M.E. — update these as goals change const NAME_GOALS = [ { label: 'Monthly Sessions', target: 3000, get: () => window.aiContext?.ga?.sessions, unit: '', color: '#00b4ff' }, { label: 'Conversions', target: 50, get: () => window.aiContext?.ga?.conversions, unit: '', color: '#00ff88' }, { label: 'Meta Spend Target', target: 5000, get: () => { const m=window.aiContext?.meta; return m?.spend ? parseFloat(String(m.spend).replace(/[^0-9.]/g,'')) : null; }, unit: '$', color: '#ff6b00' }, ]; function renderGoals() { const container = document.getElementById('sbGoals'); const block = document.getElementById('sbGoalsBlock'); if (!container || !block) return; let hasData = false; const html = NAME_GOALS.map(g => { const raw = g.get(); const current = raw != null ? parseFloat(String(raw).replace(/[^0-9.]/g,'')) : null; if (current != null && !isNaN(current)) hasData = true; const pct = (current != null && !isNaN(current)) ? Math.min(100, Math.round((current / g.target) * 100)) : 0; const dispCurrent = (current != null && !isNaN(current)) ? (g.unit === '$' ? '$'+Math.round(current).toLocaleString() : Math.round(current).toLocaleString()) : '—'; const dispTarget = g.unit === '$' ? '$'+g.target.toLocaleString() : g.target.toLocaleString(); const pctColor = pct >= 80 ? '#00ff88' : pct >= 50 ? g.color : '#ff6b00'; return `
${g.label}${pct}%
${dispCurrent}${dispTarget}
`; }).join(''); container.innerHTML = html; if (hasData) block.style.display = ''; } // Extend updateInsights to also run goals const _origUpdateInsights = updateInsights; updateInsights = function() { _origUpdateInsights(); renderGoals(); }; // Also run goals when sidebar stats update (Meta data path) const _origUpdateSidebarStats = updateSidebarStats; updateSidebarStats = function() { _origUpdateSidebarStats(); renderGoals(); }; // ── VI — IPINFO.IO CURRENT VIEWER IDENTIFICATION ────────────────────────────── // Identifies the company/org of whoever is viewing the dashboard right now const VI_IPINFO_TOKEN = 'fd144105629551'; async function identifyCurrentViewer() { const coCel = document.getElementById('viViewerCo'); const detCel = document.getElementById('viViewerDetail'); const metCel = document.getElementById('viViewerMeta'); try { const controller = new AbortController(); const tid = setTimeout(() => controller.abort(), 8000); const res = await fetch(`https://ipinfo.io?token=${VI_IPINFO_TOKEN}`, { signal: controller.signal }); clearTimeout(tid); if (!res.ok) throw new Error('ipinfo ' + res.status); const d = await res.json(); // Strip the ASN prefix from org field (e.g. "AS7922 Comcast" → "Comcast") const orgName = d.org ? d.org.replace(/^AS\d+\s+/i, '') : (d.hostname || 'Unknown Network'); const location = [d.city, d.region, d.country].filter(Boolean).join(', '); const tz = d.timezone ? ' · ' + d.timezone : ''; if (coCel) { coCel.textContent = orgName; coCel.style.color = '#fff'; } if (detCel) { detCel.textContent = location || 'Location unavailable'; detCel.style = ''; } if (metCel) metCel.textContent = `IP ${d.ip || '—'}${tz}`; window.aiContext.currentViewer = { company: orgName, ip: d.ip, location, org: d.org }; } catch(e) { // Show fallback even if lookup fails if (coCel) coCel.textContent = 'Network lookup unavailable'; if (detCel) detCel.textContent = 'Check your connection and reload'; if (metCel) metCel.textContent = 'ipinfo.io · token active'; console.warn('VI ipinfo:', e.message); } } // Run ipinfo lookup on page load; re-run when VI tab is opened setTimeout(identifyCurrentViewer, 800); const _origLoadVisitors = loadVisitors; loadVisitors = function() { identifyCurrentViewer(); return _origLoadVisitors(); }; // ── MOBILE NAVIGATION ───────────────────────────────────────────────────────── function setMobNav(key) { document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById('mn-' + key); if (btn) btn.classList.add('active'); } // ── CLIENT CAMPAIGNS / CLIENT MANAGEMENT ────────────────────────────────────── let campLoaded = false; let campClients = []; let campCurrentClient = null; let cdTrendChart = null; const PIXEL_BASE = 'https://name-analytics-worker.gentle-block-c949.workers.dev'; async function loadCampaigns() { if (campLoaded) return; campLoaded = true; try { const res = await fetch(`${WORKER}/api/clients`); const data = await res.json(); if (data.setup || data.error) { document.getElementById('campKvSetup').style.display = ''; renderClientGrid([]); return; } campClients = data.clients || []; renderClientGrid(campClients); } catch(e) { document.getElementById('campKvSetup').style.display = ''; renderClientGrid([]); } } function renderClientGrid(clients) { const grid = document.getElementById('campClientGrid'); if (!clients.length) { grid.innerHTML = `
// NO CLIENTS YET — CLICK "+ ADD CLIENT" TO ONBOARD YOUR FIRST CLIENT
`; return; } grid.innerHTML = clients.map(c => `
${c.name}
${c.industry || ''}${c.website ? ' · ' + c.website.replace('https://','').replace('http://','') : ''}
${c.pixelId ? '⬡ PIXEL ACTIVE' : ''} ${c.googleAdsCid ? 'GOOGLE ADS' : ''} ${c.metaAccountId ? 'META' : ''}
PAGE VIEWS
VISITORS
FORM FILLS
ADDED ${new Date(c.createdAt||Date.now()).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase()}
`).join(''); // Load quick stats for each client card clients.forEach(c => loadClientCardStats(c)); } async function loadClientCardStats(client) { try { const res = await fetch(`${WORKER}/api/clients/${client.id}/stats`); const data = await res.json(); const el = document.getElementById(`cstats-${client.id}`); if (!el || !data.summary) return; const s = data.summary; el.innerHTML = `
${(s.pageviews||0).toLocaleString()}
PAGE VIEWS
${(s.unique_visitors||0).toLocaleString()}
VISITORS
${(s.form_fills||0).toLocaleString()}
FORM FILLS
`; } catch(e) {} } function openClientDetail(clientId) { campCurrentClient = campClients.find(c => c.id === clientId); if (!campCurrentClient) return; document.getElementById('campClientList').style.display = 'none'; document.getElementById('campClientDetail').style.display = ''; document.getElementById('cdClientName').textContent = campCurrentClient.name; document.getElementById('cdClientMeta').textContent = [campCurrentClient.industry, campCurrentClient.website].filter(Boolean).join(' · '); // Pixel snippet const snippet = ` +d.metaSpend.toLocaleString()+' Meta spend'); if(parts.length===0)return'Connect your data sources to generate AI campaign narratives.'; return'Current period shows '+parts.join(', ')+'. Review action items below for optimization opportunities.'; } async function generateAINarrative(d){ const el=document.getElementById('mrdNarrative'); if(!el)return; try{ const prompt='In 2 sentences, analyze this campaign data for a media agency client: '+JSON.stringify(d)+'. Be specific, actionable, and professional.'; const resp=await fetch('https://name-analytics-worker.ryanr93.workers.dev/api/ai/ask',{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({question:prompt}) }); if(resp.ok){const j=await resp.json();el.textContent=j.answer||buildFallback(d);} else el.textContent=buildFallback(d); }catch(e){el.textContent=buildFallback(d);} } function triggerMeridianIntel(){ const ga=window.aiContext.ga; const meta=window.aiContext.meta; const d={}; if(ga){ d.sessions=Number(String(ga.sessions||'').replace(/,/g,''))||null; d.bounceRate=parseFloat(String(ga.bounceRate||''))||null; d.convRate=parseFloat(String(ga.conversionRate||'').replace('%',''))||null; d.ctr=d.convRate; } if(meta){ d.metaSpend=parseFloat(String(meta.spend||'').replace(/[$,]/g,''))||null; d.metaCtr=parseFloat(String(meta.ctr||'').replace('%',''))||null; if(d.metaCtr)d.ctr=d.metaCtr; } const ts=document.getElementById('mrdTimestamp'); if(ts)ts.textContent='Updated '+new Date().toLocaleTimeString(); const effEl=document.getElementById('mrdEff'); if(effEl&&d.ctr){const bench=1.0;const pct=Math.round((d.ctr/bench-1)*100);effEl.textContent=(pct>=0?'+':'')+pct+'%';effEl.style.color=pct>=0?'#00ffc8':'#ff4444';} const score=computeHealthScore(d); renderHealthScore(score); renderPacing(d); renderActionItems(d); generateAINarrative(d); } // Auto-trigger if data already loaded setTimeout(function(){if(window.aiContext.ga||window.aiContext.meta)triggerMeridianIntel();},2000); `); win.document.close(); } // ── Session state ───────────────────────────────────────────────────────────── let _aiSpeaking = false; // true while AI voice is playing let _sessionActive = false; // true while a conversation session is open let _shownFollowUp = false; // tracks if "anything else?" has been asked this session let _goodbyeTimer = null; // fires after 5s of silence let _queryAbort = null; // AbortController — cancelled when a new question interrupts async function processVoiceQuery(question) { clearTimeout(_goodbyeTimer); // Cancel any in-flight query immediately if (_queryAbort) { _queryAbort.abort(); } const ctrl = new AbortController(); _queryAbort = ctrl; document.getElementById('vpQuestion').textContent = '"' + question + '"'; document.getElementById('vpQuestion').style.display = 'block'; document.getElementById('vpAnswer').textContent = '// ANALYZING...'; const switched = await detectAndSwitchDateRange(question); if (ctrl.signal.aborted) return; if (switched) document.getElementById('vpAnswer').textContent = '// LOADING DATA...'; let answer; try { const res = await fetch(`${WORKER}/api/ai/ask`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, context: window.aiContext }), signal: ctrl.signal }); if (ctrl.signal.aborted) return; const data = await res.json(); if (data.error || !data.answer) throw new Error(data.error || 'empty'); answer = data.answer; } catch(e) { if (ctrl.signal.aborted) return; // interrupted — stop here answer = localSmartAnswer(question); } if (ctrl.signal.aborted) return; document.getElementById('vpAnswer').textContent = answer; playUISound('answer'); _aiSpeaking = true; if (!voiceMuted) await speak(cleanForSpeech(answer), ctrl.signal); _aiSpeaking = false; if (ctrl.signal.aborted) return; // Ask "anything else?" once per session, after first answer if (!_shownFollowUp && _sessionActive) { _shownFollowUp = true; await new Promise(r => setTimeout(r, 800)); if (ctrl.signal.aborted) return; const fq = 'Is there anything else you need help with?'; document.getElementById('vpAnswer').textContent = fq; document.getElementById('vpQuestion').style.display = 'none'; _aiSpeaking = true; if (!voiceMuted) await speak(fq, ctrl.signal); _aiSpeaking = false; if (ctrl.signal.aborted) return; } // Keep mic open — 5s silence closes the session if (_sessionActive) { setListening(true); _goodbyeTimer = setTimeout(async () => { _sessionActive = false; _shownFollowUp = false; recognition.stop(); setListening(false); const bye = "If you need anything else, I'm one tap away."; document.getElementById('vpAnswer').textContent = bye; document.getElementById('vpQuestion').style.display = 'none'; _aiSpeaking = true; if (!voiceMuted) await speak(bye, null); _aiSpeaking = false; setTimeout(() => { const panel = document.getElementById('voicePanel'); if (panel) panel.classList.remove('show'); }, 3000); }, 5000); } } // ── Date range detection ─────────────────────────────────────────────────────── const DATE_RANGE_PATTERNS = [ { pattern: /last\s+(7|seven)\s+day|past\s+(7|seven)\s+day|last\s+week|past\s+week/i, preset: 'last_7d' }, { pattern: /last\s+(30|thirty)\s+day/i, preset: 'last_30d' }, { pattern: /last\s+(90|ninety)\s+day|last\s+quarter|past\s+quarter/i, preset: 'last_90d' }, { pattern: /this\s+month|current\s+month|month\s+to\s+date|mtd/i, preset: 'this_month' }, { pattern: /last\s+month|previous\s+month/i, preset: 'last_month' }, { pattern: /last\s+year|past\s+year|365/i, preset: 'last_year' }, ]; function wordsToNum(str) { const map = { one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10, eleven:11,twelve:12,thirteen:13,fourteen:14,fifteen:15,sixteen:16,seventeen:17, eighteen:18,nineteen:19,twenty:20,'twenty-one':21,'twenty-two':22,'twenty-three':23, 'twenty-four':24,'twenty-five':25,thirty:30,forty:40,fifty:50,sixty:60, seventy:70,eighty:80,ninety:90,hundred:100 }; const n = parseInt(str); if (!isNaN(n)) return n; const lower = str.toLowerCase().trim(); if (map[lower] !== undefined) return map[lower]; const parts = lower.split(/[\s-]+/); if (parts.length === 2 && map[parts[0]] !== undefined && map[parts[1]] !== undefined) return map[parts[0]] + map[parts[1]]; return null; } // Set an exact custom day range (any number of days) and reload all data function setCustomDaysRange(n) { const fmtDate = d => d.toISOString().split('T')[0]; const end = new Date(); const start = new Date(); start.setDate(start.getDate() - n); window.dateRange = { preset: 'custom', start: fmtDate(start), end: fmtDate(end), label: `LAST ${n} DAYS` }; // Sync the UI picker and show custom inputs with the computed values const picker = document.getElementById('dateRangePicker'); if (picker) picker.value = 'custom'; const cdr = document.getElementById('customDateRange'); if (cdr) cdr.style.display = 'flex'; const si = document.getElementById('customStart'); const ei = document.getElementById('customEnd'); if (si) si.value = fmtDate(start); if (ei) ei.value = fmtDate(end); refreshAll(); } async function switchToPreset(preset) { if (window.dateRange && window.dateRange.preset !== preset) { const picker = document.getElementById('dateRangePicker'); if (picker) { picker.value = preset; onDateRangeChange(preset); } await new Promise(resolve => setTimeout(resolve, 2800)); } } async function detectAndSwitchDateRange(question) { // Check specific named presets first (last week, last month, etc.) for (const { pattern, preset } of DATE_RANGE_PATTERNS) { if (pattern.test(question)) { await switchToPreset(preset); return true; } } // "last/past N days" — exact day count, any number (3, 13, 22, forty-five, etc.) const dayMatch = question.match(/(?:last|past)\s+([\w\s-]+?)\s+days?/i); if (dayMatch) { const n = wordsToNum(dayMatch[1].trim()); if (n && n > 0) { setCustomDaysRange(n); await new Promise(resolve => setTimeout(resolve, 2800)); return true; } } // "last/past N weeks" — converts to exact days const weekMatch = question.match(/(?:last|past)\s+([\w]+)\s+weeks?/i); if (weekMatch) { const n = wordsToNum(weekMatch[1].trim()); if (n && n > 0) { setCustomDaysRange(n * 7); await new Promise(resolve => setTimeout(resolve, 2800)); return true; } } // "last/past N months" — converts to exact days const monthMatch = question.match(/(?:last|past)\s+([\w]+)\s+months?/i); if (monthMatch) { const n = wordsToNum(monthMatch[1].trim()); if (n && n > 0) { setCustomDaysRange(n * 30); await new Promise(resolve => setTimeout(resolve, 2800)); return true; } } return false; } function localSmartAnswer(q) { const lower = q.toLowerCase(); const ctx = window.aiContext || {}; const ga = ctx.ga || {}; const meta = ctx.meta || {}; const li = ctx.linkedin || {}; const vis = ctx.visitors || {}; const period = ga.period || meta.period || 'this period'; const has = (...words) => words.some(w => lower.includes(w)); // ── Traffic performance (broad overview) ───────────────────────────────── if (has('traffic performance','website traffic','site traffic','web traffic', 'traffic report','how is the site','site doing','website doing')) { if (ga.sessions) { let resp = `Website traffic for ${period}: ${Number(ga.sessions).toLocaleString()} sessions from ${Number(ga.users).toLocaleString()} active users, ${Number(ga.views).toLocaleString()} page views, ${ga.pagesPerUser} pages per user, and a ${ga.conversionRate} conversion rate. Your top traffic channel is ${ga.topChannel}.`; if (ga.bounceRate) resp += ` Bounce rate is ${ga.bounceRate}.`; if (ga.avgDuration) resp += ` Average session duration is ${ga.avgDuration}.`; const sessions = Number(ga.sessions); if (sessions < 500) resp += ' Traffic is on the lighter side — consider boosting it with paid search, social ads, or a content push targeting your top keywords.'; else if (sessions >= 500 && sessions < 2000) resp += ' Solid traffic volume. To accelerate growth, consider scaling your best-performing ad channel or publishing more SEO-targeted content.'; else resp += ' Strong traffic volume. Focus on improving conversion rate and reducing bounce rate to maximize the value of this audience.'; return resp; } return 'Switch to the GA4 Analytics tab first so I can load your traffic data, then ask again.'; } // ── Bounce rate ──────────────────────────────────────────────────────────── if (has('bounce','bounce rate')) { if (ga.bounceRate) { const br = parseFloat(ga.bounceRate); const grade = br < 40 ? 'Excellent — visitors are highly engaged.' : br < 55 ? 'Good — within a healthy range.' : br < 70 ? 'This is average — consider improving landing page engagement.' : 'This is high — your landing pages may need optimization.'; const tip = br >= 55 ? ' To bring this down, consider faster page load times, stronger above-the-fold headlines, or clearer calls to action on your landing pages.' : ' To keep engagement strong, continue matching your ad messaging to what visitors find on the landing page.'; return `Your bounce rate is ${ga.bounceRate} for ${period}. ${grade} Industry average is typically 45 to 65 percent.${tip}`; } return 'Switch to the analytics tab first to load your bounce rate data, then ask again.'; } // ── Session duration / time on site ─────────────────────────────────────── if (has('session duration','time on','time spent','how long','average time','dwell','time per')) { if (ga.avgDuration) { const dur = parseFloat(ga.avgDuration); const tip = dur < 60 ? ' To increase time on site, consider adding more engaging content, video, or interactive elements that keep visitors exploring.' : ' Strong session duration — your content is keeping visitors engaged. Consider adding internal links to guide them toward a conversion.'; return `Your average session duration is ${ga.avgDuration} for ${period}. This reflects how long visitors spend on your site per visit.${tip}`; } return 'Switch to the analytics tab first to load session duration data, then ask again.'; } // ── New vs returning visitors ────────────────────────────────────────────── if (has('new user','new visitor','return visitor','returning','repeat visit','loyal','new vs')) { if (ga.newUsersPct) return `${period}: ${ga.newUsersPct} of your visitors are new users and ${ga.returningPct} are returning visitors. You had ${Number(ga.newUsers||0).toLocaleString()} new users out of ${Number(ga.users).toLocaleString()} total.`; return 'Open the GA4 tab to see new vs returning visitor data.'; } // ── Top pages / landing pages ───────────────────────────────────────────── if (has('top page','best page','most visited','popular page','landing page','most traffic page','which page','highest traffic page')) { if (ga.topPages && ga.topPages.length) { const top = ga.topPages.slice(0,3).map((p,i)=>{ const label = p.path==='/'?'Homepage':p.path.split('/').filter(Boolean).pop().replace(/-/g,' ').replace(/\b\w/g,c=>c.toUpperCase()); return `${i+1}. ${label} (${Number(p.views).toLocaleString()} views)`; }).join(', '); return `Your top pages ${period}: ${top}. If these pages have strong traffic but low conversions, consider adding clearer calls to action or a lead capture form above the fold.`; } return 'Open the GA4 tab to see your top performing pages.'; } // ── Geographic / location data ──────────────────────────────────────────── if (has('city','cities','location','geography','where are my visitor','country','state','region','geo','where is traffic coming')) { if (ga.topCities && ga.topCities.length) { const cities = ga.topCities.slice(0,4).map(c=>`${c.city} (${Number(c.users).toLocaleString()})`).join(', '); return `Your top visitor locations ${period} by city: ${cities}.`; } return 'Open the GA4 tab to see geographic breakdown of your visitors.'; } // ── Device breakdown ────────────────────────────────────────────────────── if (has('device','mobile','desktop','tablet','what device','which device','phone','laptop','screen size','mobile traffic','mobile vs')) { if (ga.devices && Object.keys(ga.devices).length) { const sorted = Object.entries(ga.devices).sort((a,b)=>b[1]-a[1]); const parts = sorted.map(([d,pct])=>`${d}: ${pct}%`).join(', '); const topDev = sorted[0]; const tip = topDev && topDev[0]==='mobile' && topDev[1]>50 ? ' Majority mobile — make sure your site is fully mobile-optimized.' : ''; return `Device breakdown ${period}: ${parts}.${tip}`; } return 'Open the GA4 tab to see device breakdown.'; } // ── SEO performance ─────────────────────────────────────────────────────── if (has('seo','search engine optimization','organic search','search ranking','google ranking','keyword','search perform','how is seo','seo doing')) { if (ga.allChannels && ga.allChannels.length) { const organic = ga.allChannels.find(c=>c.channel.toLowerCase().includes('organic search')); if (organic) { const verdict = organic.pct >= 40 ? 'Strong organic presence.' : organic.pct >= 20 ? 'Moderate organic traffic — room to grow.' : 'Low organic traffic — consider investing in content and SEO.'; return `Organic search (SEO) drove ${Number(organic.sessions).toLocaleString()} sessions (${organic.pct}% of all traffic) ${period}. ${verdict}`; } } if (ga.topChannel && ga.topChannel.toLowerCase().includes('organic')) return `Organic Search is your top channel ${period} with ${Number(ga.sessions||0).toLocaleString()} total sessions.`; return 'Open the GA4 tab. I can then analyze your organic search channel performance.'; } // ── ROAS / ROI ──────────────────────────────────────────────────────────── if (has('roas','return on ad','roi','return on invest','ad return')) { if (meta.spend && ga.conversions && parseInt(ga.conversions) > 0) { const spendNum = parseFloat((meta.spend||'$0').replace(/[$,]/g,'')); const cpa = spendNum > 0 ? `$${(spendNum/parseInt(ga.conversions)).toFixed(2)}` : 'N/A'; return `${period}: ${meta.spend} Meta ad spend generated ${Number(ga.conversions).toLocaleString()} conversions — a cost per conversion of ${cpa}. For true ROAS calculation, connect revenue values to your GA4 conversion events.`; } return 'Open both the Meta Ads and GA4 tabs. ROAS requires both spend and conversion data.'; } // ── Cost per conversion / CPL / CPA ────────────────────────────────────── if (has('cost per conversion','cost per lead','cpl','cost per acqui','cost per result','cpa')) { const parts = []; if (meta.spend && ga.conversions && parseInt(ga.conversions) > 0) { const spendNum = parseFloat((meta.spend||'$0').replace(/[$,]/g,'')); if (spendNum > 0) parts.push(`Meta cost per conversion: $${(spendNum/parseInt(ga.conversions)).toFixed(2)}`); } if (li.spend && li.conversions && parseInt(li.conversions) > 0) { const liSpend = parseFloat((li.spend||'$0').replace(/[$,]/g,'')); if (liSpend > 0) parts.push(`LinkedIn cost per conversion: $${(liSpend/parseInt(li.conversions)).toFixed(2)}`); } if (parts.length) return `${parts.join('. ')} ${period}.`; return 'Open both the ads tab and GA4 tab to calculate cost per conversion.'; } // ── Impressions / reach ─────────────────────────────────────────────────── if (has('impression','reach','how many people saw','exposure','how many saw')) { const parts = []; if (meta.impressions) parts.push(`Meta: ${Number(meta.impressions).toLocaleString()} impressions, ${Number(meta.reach||0).toLocaleString()} reach`); if (li.impressions) parts.push(`LinkedIn: ${Number(li.impressions).toLocaleString()} impressions`); if (parts.length) return `Ad impressions ${period} — ${parts.join('. ')}.`; return 'Open the Meta or LinkedIn Ads tabs to see impression and reach data.'; } // ── Ad frequency / fatigue ──────────────────────────────────────────────── if (has('frequency','ad fatigue','showing too many','ad too often','seeing my ad too','repeat ad')) { if (meta.freq) { const f = parseFloat(meta.freq); const warn = f >= 4 ? ' Warning: frequency above 4 usually causes ad fatigue — consider refreshing your creative.' : f >= 3 ? ' Approaching ad fatigue threshold — monitor CTR for signs of decline.' : ' Frequency is healthy.'; return `Your Meta ad frequency is ${meta.freq} ${period} — each person saw your ads an average of ${meta.freq} times.${warn}`; } return 'Open the Meta Ads tab to see frequency data.'; } // ── CPM ─────────────────────────────────────────────────────────────────── if (has('cpm','cost per thousand','cost per mille','cost per impression')) { const parts = []; if (meta.cpm) parts.push(`Meta CPM is ${meta.cpm}`); if (li.cpm) parts.push(`LinkedIn CPM is ${li.cpm}`); if (parts.length) return `${parts.join('. ')} ${period}. This is the cost per 1,000 ad impressions.`; return 'Open the Meta or LinkedIn Ads tabs to see CPM data.'; } // ── Campaign performance ────────────────────────────────────────────────── if (has('campaign','best campaign','top campaign','which ad is','best ad','ad perform','which campaign')) { if (meta.spend) return `Meta campaigns ${period}: ${meta.spend} total spend, ${Number(meta.impressions||0).toLocaleString()} impressions, ${Number(meta.clicks||0).toLocaleString()} clicks at ${meta.ctr} CTR. Open the Meta Ads tab for the full campaign-by-campaign breakdown.`; return 'Open the Meta Ads tab to see campaign performance details.'; } // ── Daily spend rate ────────────────────────────────────────────────────── if (has('per day','daily spend','daily budget','daily average','spending per day','spend per day')) { if (meta.spend) { const dr = window.dateRange || {}; let days = 30; if (dr.preset === 'last_7d') days = 7; else if (dr.preset === 'last_90d') days = 90; else if (dr.preset === 'last_year') days = 365; else if (dr.preset === 'custom' && dr.start && dr.end) { days = Math.max(1, Math.round((new Date(dr.end) - new Date(dr.start)) / 86400000)); } const spendNum = parseFloat((meta.spend||'$0').replace(/[$,]/g,'')); return `Your average daily Meta ad spend ${period} is $${(spendNum/days).toFixed(2)} (${meta.spend} total over ~${days} days).`; } return 'Open the Meta Ads tab to calculate daily spend.'; } // ── Traffic diagnosis ───────────────────────────────────────────────────── if (has('why is traffic','why did traffic','traffic drop','traffic down','traffic declin','traffic fell','traffic slow','why less','why fewer')) { if (ga.sessions && ga.allChannels) { const top2 = ga.allChannels.slice(0,2).map(c=>`${c.channel} (${c.pct}%)`).join(' and '); return `Current data shows ${Number(ga.sessions).toLocaleString()} sessions ${period} driven primarily by ${top2}. Traffic drops typically stem from: reduced organic rankings, lower ad spend, seasonality, or a technical issue like a broken page. Check the trend chart on the GA4 tab to pinpoint exactly when the drop started.`; } return 'Open the GA4 tab. The session trend chart will show you exactly when traffic changed.'; } // ── Strategy / recommendations ──────────────────────────────────────────── if (has('should i','recommend','what should i','strategy','advice','suggest','what would you','increase budget','increase spend','optimize','improve')) { const advice = []; if (meta.ctr) { const ctr=parseFloat(meta.ctr); if(!isNaN(ctr)&&ctr<1) advice.push('your Meta CTR is below 1% — refresh your ad creative before increasing budget'); else if(!isNaN(ctr)&&ctr>=2) advice.push('your Meta CTR is strong — scaling budget could increase results proportionally'); } if (ga.bounceRate) { const br=parseFloat(ga.bounceRate); if(!isNaN(br)&&br>65) advice.push('your bounce rate is high — optimizing your landing page could significantly improve conversions'); } if (ga.conversionRate) { const cr=parseFloat(ga.conversionRate); if(!isNaN(cr)&&cr<1) advice.push('your conversion rate is under 1% — A/B testing your call-to-action and form placement is recommended'); } if (ga.devices && ga.devices.mobile > 55 && ga.avgDuration) { const d=ga.avgDuration; if(d.includes('s')&&!d.includes('m')&&parseInt(d)<60) advice.push('most traffic is mobile but session duration is short — check your mobile page speed'); } if (advice.length) return `Based on your data ${period}: ${advice.join('; ')}. Always test one change at a time to measure impact.`; return 'Load your GA4 and Meta data first. Then I can give you data-driven recommendations based on your specific numbers.'; } // ── Period comparison ───────────────────────────────────────────────────── if (has('compare','vs last','versus last','this month vs','this week vs','last month vs','month over month','week over week','compare period','how does this')) { return `To compare periods, use the date range picker at the top of the dashboard — switch between Last 7 Days, Last 30 Days, and Last 90 Days and note the numbers. For side-by-side comparison, I recommend asking me for a summary after each date range selection.`; } // ── Channel breakdown detail ─────────────────────────────────────────────── if (has('channel breakdown','all channel','traffic breakdown','all source','where is my traffic','where is all my traffic','traffic coming from','traffic by channel','traffic source')) { if (ga.allChannels && ga.allChannels.length) { const breakdown = ga.allChannels.slice(0,6).map(c=>`${c.channel}: ${c.pct}%`).join(', '); return `Traffic channel breakdown ${period}: ${breakdown}. Total sessions: ${Number(ga.sessions||0).toLocaleString()}.`; } return 'Open the GA4 tab to see the full channel breakdown.'; } // ── Sessions / visits ───────────────────────────────────────────────────── if (has('session','visit','how many people','how much traffic')) { if (ga.sessions) return `You had ${Number(ga.sessions).toLocaleString()} sessions and ${Number(ga.users).toLocaleString()} active users ${period}. Top channel: ${ga.topChannel || 'Direct'}. Bounce rate: ${ga.bounceRate || 'N/A'}. Avg session: ${ga.avgDuration || 'N/A'}.`; return 'GA4 data is still loading. Switch to the GA4 tab and wait a moment.'; } // ── Users ───────────────────────────────────────────────────────────────── if (has('user','how many visitor','active user')) { if (ga.users) return `You had ${Number(ga.users).toLocaleString()} active users ${period}. ${ga.newUsersPct ? `${ga.newUsersPct} were new and ${ga.returningPct} returning.` : ''}`; return 'Open the GA4 tab to see user data.'; } // ── Page views ──────────────────────────────────────────────────────────── if (has('page view','pageview','pages per')) { if (ga.views) return `You recorded ${Number(ga.views).toLocaleString()} page views ${period}, averaging ${ga.pagesPerUser} pages per user.`; return 'Switch to the GA4 tab to load page view data.'; } // ── Conversion details — what were the actual conversions? ──────────────── if (has('what were','which conversion','what type of conversion','what kind of conversion', 'what were the conversion','tell me about the conversion','conversion detail', 'what converted','what did convert','which page converted','conversion breakdown', 'what lead','what form','what signup','what purchase','what type of lead')) { if (ga.conversionDetails && ga.conversionDetails.length) { const details = ga.conversionDetails.slice(0,5).map(d=>{ const c = d.count > 1 ? `${d.count}x` : ''; return `${d.eventLabel}${c ? ' ('+c+')' : ''} on ${d.pageLabel}`; }).join('; '); const total = ga.conversionDetails.reduce((s,d)=>s+d.count,0); return `Here are your ${total} conversion${total!==1?'s':''} ${period}: ${details}.`; } if (ga.conversions !== undefined && parseInt(ga.conversions) === 0) return `You had no conversions ${period}. Check that your GA4 conversion events are configured correctly.`; return 'Open the GA4 tab first so I can load your conversion event details.'; } // ── Which pages are converting? ─────────────────────────────────────────── if (has('which page convert','what page convert','page that convert','best converting page','highest converting')) { if (ga.conversionDetails && ga.conversionDetails.length) { const byPage = {}; ga.conversionDetails.forEach(d=>{ byPage[d.pageLabel]=(byPage[d.pageLabel]||0)+d.count; }); const sorted = Object.entries(byPage).sort((a,b)=>b[1]-a[1]); const top = sorted.slice(0,3).map(([page,cnt])=>`${page} (${cnt})`).join(', '); return `Your converting pages ${period}: ${top}.`; } return 'Open the GA4 tab to load conversion page data.'; } // ── Conversions ─────────────────────────────────────────────────────────── if (has('conversion','convert','lead','goal')) { if (ga.conversions !== undefined) { let resp = `You had ${Number(ga.conversions).toLocaleString()} conversion${ga.conversions!=1?'s':''} with a ${ga.conversionRate} conversion rate ${period}.`; if (ga.conversionDetails && ga.conversionDetails.length) { const top = ga.conversionDetails[0]; resp += ` Top conversion event: ${top.eventLabel} on ${top.pageLabel}.`; } const cvr = parseFloat(ga.conversionRate); if (!isNaN(cvr)) { if (cvr < 1) resp += ' A conversion rate below 1% typically signals a landing page or messaging issue — consider A/B testing your headline, CTA button, or offer.'; else if (cvr < 3) resp += ' Your conversion rate is solid. To push it higher, try adding social proof, tightening your call to action, or reducing friction in your form.'; else resp += ' That is an excellent conversion rate. To scale results further, consider increasing ad spend on your top traffic sources.'; } return resp; } return 'No conversion data loaded yet. Open the GA4 tab first.'; } // ── Channels / sources ──────────────────────────────────────────────────── if (has('channel','source','organic','paid','social','email','direct','referral')) { if (ga.topChannel) return `Your top traffic channel is ${ga.topChannel} ${period}. Total sessions: ${Number(ga.sessions||0).toLocaleString()}.`; return 'Open the GA4 tab to see channel breakdown.'; } // ── Meta ads ────────────────────────────────────────────────────────────── if (has('meta','facebook','fb','instagram')) { if (meta.spend) return `Meta Ads ${period}: ${meta.spend} spent, ${Number(meta.impressions||0).toLocaleString()} impressions, ${Number(meta.clicks||0).toLocaleString()} clicks, ${meta.ctr} CTR, ${meta.cpc} CPC. Frequency: ${meta.freq || 'N/A'}.`; return 'Open the Meta Ads tab to load your Facebook and Instagram data.'; } // ── LinkedIn ────────────────────────────────────────────────────────────── if (has('linkedin','linked in')) { if (li.spend) return `LinkedIn Ads ${period}: ${li.spend} spent, ${Number(li.impressions||0).toLocaleString()} impressions, ${li.ctr} CTR, ${li.conversions} conversions.`; return 'LinkedIn Ads data is pending API approval. Check back soon.'; } // ── Spend / budget ──────────────────────────────────────────────────────── if (has('spend','budget','money','dollar','how much','paid')) { const parts = []; if (meta.spend) parts.push(`Meta: ${meta.spend}`); if (li.spend) parts.push(`LinkedIn: ${li.spend}`); if (parts.length) return `Ad spend ${period} — ${parts.join(', ')}.`; return 'Open the Meta or LinkedIn Ads tabs to load spend data.'; } // ── CTR ─────────────────────────────────────────────────────────────────── if (has('ctr','click through','click-through')) { const parts = []; if (meta.ctr) parts.push(`Meta CTR: ${meta.ctr}`); if (li.ctr) parts.push(`LinkedIn CTR: ${li.ctr}`); if (parts.length) { let resp = `${parts.join('. ')} for ${period}.`; const ctrVal = meta.ctr ? parseFloat(meta.ctr) : null; if (ctrVal !== null && !isNaN(ctrVal)) { if (ctrVal < 1) resp += ' A CTR below 1% on Meta usually means the creative or audience needs work — try refreshing the ad visuals or testing a new hook in the first 3 seconds.'; else if (ctrVal < 2) resp += ' Decent CTR. To push it higher, test new ad creatives and make sure your headline speaks directly to your target audience pain point.'; else resp += ' Strong CTR. Your ads are resonating well — now focus on making sure the landing page converts that interest into leads.'; } return resp; } return 'Load the Meta or LinkedIn Ads tabs to see CTR data.'; } // ── CPC ─────────────────────────────────────────────────────────────────── if (has('cpc','cost per click')) { const parts = []; if (meta.cpc) parts.push(`Meta CPC: ${meta.cpc}`); if (li.cpc) parts.push(`LinkedIn CPC: ${li.cpc}`); if (parts.length) return `${parts.join('. ')} ${period}.`; return 'Load the Meta or LinkedIn Ads tabs to see cost per click.'; } // ── Visitors / company identification ───────────────────────────────────── if (has('visitor intel','who is visiting','identify visitor','rb2b','company visit')) { if (vis.total) return `Visitor Intel has identified ${vis.total} companies visiting your site, with ${vis.today} visits today and ${vis.people} people tracked.`; return 'Open the Visitor Intel tab to see which companies are visiting your site.'; } // ── General performance / summary ───────────────────────────────────────── if (has('perform','summary','overview','how are we doing','how is everything','report','everything','all metrics')) { const parts = []; if (ga.sessions) parts.push(`GA4: ${Number(ga.sessions).toLocaleString()} sessions, ${ga.conversionRate} conversion rate, ${ga.bounceRate||'N/A'} bounce rate`); if (meta.spend) parts.push(`Meta Ads: ${meta.spend} spend, ${meta.ctr} CTR`); if (li.spend) parts.push(`LinkedIn: ${li.spend} spend`); if (parts.length) return `Performance summary ${period}: ${parts.join(' | ')}.`; return 'Open each tab to load data, then ask for a summary.'; } // ── Help ────────────────────────────────────────────────────────────────── if (has('help','what can you','what do you','capable','what questions')) { return 'I can answer questions about: website traffic, sessions, bounce rate, session duration, new vs returning visitors, top pages, devices, city data, SEO performance, Meta and LinkedIn ad spend, CTR, CPC, CPM, ROAS, cost per lead, ad frequency, campaign performance, traffic diagnosis, and budget strategy. Try: "What is my bounce rate?" or "Show me device breakdown" or "Why is my traffic down?"'; } // ── General fallback ────────────────────────────────────────────────────── const parts = []; if (ga.sessions) parts.push(`${Number(ga.sessions).toLocaleString()} sessions, ${ga.conversionRate} conversion rate, ${ga.bounceRate||''} bounce rate`); if (meta.spend) parts.push(`Meta: ${meta.spend} spend, ${meta.ctr} CTR`); if (li.spend) parts.push(`LinkedIn: ${li.spend} spend`); if (parts.length) return `Here is your performance summary for ${period}: ${parts.join('. ')}.`; return `I can help with website traffic, bounce rate, session duration, top pages, device breakdown, ad spend, conversions, and more. Make sure the relevant tab is loaded first so I have the data to pull from.`; } // ── Clean text before speaking (strips UI decorators, adds friendly OUTRO) ────── function cleanForSpeech(text) { return text .replace(/\/\//g, '') // remove // decorators .replace(/\bMTD:\s*/gi, 'month to date, ') // MTD → readable .replace(/\s*→\s*/g, ' to ') // → arrows .replace(/\|/g, ',') // pipe separators .replace(/\bTry asking about[^.!?]*/gi, '') // remove "Try asking about..." .replace(/\bFeel free to ask[^.!?]*/gi, '') // remove "Feel free to..." .replace(/\bMake sure the relevant tab[^.!?]*/gi, '') .replace(/\bOpen the GA4 tab\b/gi, 'Open the analytics tab') .replace(/\s{2,}/g, ' ') .trim() ; } // ── Voice: ElevenLabs (realistic) with browser TTS fallback ─────────────────── // ── Audio context (bypasses autoplay restrictions after long async chains) ── let _audioCtx = null; function getAudioCtx() { if (!_audioCtx || _audioCtx.state === 'closed') { _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (_audioCtx.state === 'suspended') _audioCtx.resume(); return _audioCtx; } // Unlock AudioContext on every user interaction document.addEventListener('click', () => { try { getAudioCtx().resume(); } catch(e) {} }, true); // ── UI SOUND ENGINE ─────────────────────────────────────────────────────────── function playUISound(type) { if (voiceMuted) return; try { const ctx = getAudioCtx(); if (type === 'activate') { // Rising 3-note activate when orb is tapped const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(440, ctx.currentTime); osc.frequency.linearRampToValueAtTime(880, ctx.currentTime + 0.12); osc.frequency.linearRampToValueAtTime(1320, ctx.currentTime + 0.22); gain.gain.setValueAtTime(0, ctx.currentTime); gain.gain.linearRampToValueAtTime(0.08, ctx.currentTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35); osc.start(); osc.stop(ctx.currentTime + 0.35); } else if (type === 'deactivate') { // Falling deactivate when orb is tapped to stop const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1320, ctx.currentTime); osc.frequency.linearRampToValueAtTime(440, ctx.currentTime + 0.2); gain.gain.setValueAtTime(0.06, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3); osc.start(); osc.stop(ctx.currentTime + 0.3); } else if (type === 'answer') { // Soft two-note chime when AI finishes answering [0, 0.18].forEach((delay, i) => { const o = ctx.createOscillator(); const g = ctx.createGain(); o.connect(g); g.connect(ctx.destination); o.type = 'sine'; o.frequency.setValueAtTime(i === 0 ? 660 : 880, ctx.currentTime + delay); g.gain.setValueAtTime(0.05, ctx.currentTime + delay); g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + delay + 0.45); o.start(ctx.currentTime + delay); o.stop(ctx.currentTime + delay + 0.45); }); } else if (type === 'nav') { // Subtle tick when clicking sidebar nav const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1100, ctx.currentTime); osc.frequency.linearRampToValueAtTime(800, ctx.currentTime + 0.06); gain.gain.setValueAtTime(0.045, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1); osc.start(); osc.stop(ctx.currentTime + 0.1); } else if (type === 'hover') { // Ultra-subtle hover blip const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1800, ctx.currentTime); gain.gain.setValueAtTime(0.018, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05); osc.start(); osc.stop(ctx.currentTime + 0.05); } } catch(e) {} } // Wire hover + click sounds to interactive elements after DOM ready function initInteractionSounds() { document.querySelectorAll('.sb-nav-item').forEach(el => { if (!el) return; el.addEventListener('mouseenter', () => playUISound('hover')); el.addEventListener('click', () => playUISound('nav')); }); document.querySelectorAll('.mute-hdr-btn, .sb-report-btn, .dr-preset-btn, .dr-apply-btn, .vp-close-btn, .report-dl-btn, .voice-type-send').forEach(el => { if (!el) return; el.addEventListener('mouseenter', () => playUISound('hover')); }); } // Run after full page load to ensure all elements exist window.addEventListener('load', initInteractionSounds); function toggleMute() { voiceMuted = !voiceMuted; const lbl = document.getElementById('muteLabel'); const btn = document.getElementById('muteHdrBtn'); if (voiceMuted) { if (currentAudio) { try { currentAudio.pause(); } catch(e) {} currentAudio = null; } if (window.speechSynthesis) window.speechSynthesis.cancel(); if (lbl) lbl.textContent = 'UNMUTE'; if (lbl) lbl.textContent = 'SOUND OFF'; if (btn) btn.style.opacity = '0.4'; } else { if (lbl) lbl.textContent = 'MUTE'; if (btn) btn.style.opacity = '1'; } } function closeVoicePanel() { // Stop all audio immediately if (currentAudio) { try { currentAudio.pause(); } catch(e) {} currentAudio = null; } if (window.speechSynthesis) window.speechSynthesis.cancel(); // Abort any in-flight query if (_queryAbort) { _queryAbort.abort(); _queryAbort = null; } _aiSpeaking = false; _sessionActive = false; _shownFollowUp = false; clearTimeout(_goodbyeTimer); _goodbyeTimer = null; if (recognition) try { recognition.stop(); } catch(e) {} setListening(false); document.getElementById('voicePanel').classList.remove('show'); } // speak() accepts an optional AbortSignal so it can be cancelled mid-playback async function speak(text, signal) { if (voiceMuted || !text) return; if (signal?.aborted) return; try { const res = await fetch(`${WORKER}/api/tts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), signal }); if (signal?.aborted) return; if (res.ok) { const arrayBuf = await res.arrayBuffer(); if (signal?.aborted) return; const ctx = getAudioCtx(); if (ctx.state === 'suspended') await ctx.resume(); if (signal?.aborted) return; const decoded = await ctx.decodeAudioData(arrayBuf); const src = ctx.createBufferSource(); src.buffer = decoded; src.connect(ctx.destination); currentAudio = { pause: () => { try { src.stop(); } catch(e) {} } }; await new Promise(resolve => { // Resolve immediately if aborted before playback starts if (signal) signal.addEventListener('abort', () => { try { src.stop(); } catch(e) {} currentAudio = null; resolve(); }, { once: true }); src.onended = () => { currentAudio = null; resolve(); }; src.start(0); }); return; } } catch(e) { if (signal?.aborted) return; console.warn('TTS error:', e); } if (signal?.aborted) return; await speakBrowser(text, signal); } function getBestVoice() { const voices = window.speechSynthesis.getVoices(); if (!voices.length) return null; const priority = [ 'Ava (Enhanced)', 'Ava (Premium)', 'Ava', 'Allison (Enhanced)', 'Allison (Premium)', 'Allison', 'Samantha (Enhanced)', 'Samantha (Premium)', 'Susan (Enhanced)', 'Karen (Enhanced)', 'Karen', 'Victoria (Enhanced)', 'Moira', 'Google US English', 'Microsoft Aria Online (Natural) - English (United States)', 'Microsoft Zira - English (United States)', 'Samantha' ]; for (const name of priority) { const v = voices.find(v => v.name === name); if (v) return v; } const enhanced = voices.find(v => v.lang.startsWith('en') && /enhanced|premium/i.test(v.name)); if (enhanced) return enhanced; return voices.find(v => v.lang === 'en-US') || voices.find(v => v.lang.startsWith('en')); } function speakBrowser(text, signal) { if (!window.speechSynthesis) return Promise.resolve(); if (signal?.aborted) return Promise.resolve(); window.speechSynthesis.cancel(); const utt = new SpeechSynthesisUtterance(text); utt.rate = 0.91; utt.pitch = 1.0; utt.volume = 1.0; return new Promise(resolve => { utt.onend = resolve; utt.onerror = resolve; // If aborted before 80ms timer fires, cancel and resolve immediately if (signal) signal.addEventListener('abort', () => { window.speechSynthesis.cancel(); resolve(); }, { once: true }); const trySpeak = () => { if (signal?.aborted) { resolve(); return; } // aborted during the 80ms window const voice = getBestVoice(); if (voice) utt.voice = voice; window.speechSynthesis.speak(utt); }; if (window.speechSynthesis.getVoices().length) { setTimeout(trySpeak, 80); } else { window.speechSynthesis.addEventListener('voiceschanged', () => setTimeout(trySpeak, 80), { once: true }); } }); } // Pre-load voices async if (window.speechSynthesis) { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = () => window.speechSynthesis.getVoices(); } // ── VISITOR INTELLIGENCE ──────────────────────────────────────────────────────── function renderVisitorCard(v) { const div = document.createElement('div'); div.className = 'vis-card'; div.innerHTML = `
${v.company}
${v.industry}
${v.person}
${v.title}
${v.linkedIn ? `🔗 View LinkedIn Profile` : ''}
${v.size || ''}
`; return div; } async function loadVisitors() { if (visitorsLoaded) return; visitorsLoaded = true; const s = (id, v) => { const e = document.getElementById(id); if(e) e.style.display = v; }; s('visLoading', 'none'); s('visLive', 'none'); const showLive = (visitors, errorMsg) => { const grid = document.getElementById('visGrid'); if (!grid) return; grid.innerHTML = ''; const today = new Date().toDateString(); const todayCount = visitors.filter(v => new Date(v.when).toDateString() === today).length; const uniqueCompanies = new Set(visitors.map(v => v.company)).size; const peopleCount = visitors.filter(v => v.person).length; const kpiTotal = document.getElementById('vKpiTotal'); const kpiToday = document.getElementById('vKpiToday'); const kpiPeople = document.getElementById('vKpiPeople'); if (kpiTotal) kpiTotal.textContent = uniqueCompanies; if (kpiToday) kpiToday.textContent = todayCount; if (kpiPeople) kpiPeople.textContent = peopleCount; if (visitors.length) { visitors.forEach(v => { const relTime = formatRelTime(v.when); grid.appendChild(renderVisitorCard({ ...v, when: relTime })); }); } else { grid.innerHTML = `
${errorMsg || '// NO VISITORS IDENTIFIED YET — PIXEL IS ACTIVE AND WARMING UP'}
`; } const ts = new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',timeZoneName:'short'}); const ftr = document.getElementById('visFtr'); if (ftr) ftr.textContent = `// VISITOR DATA · LAST UPDATED ${ts} //`; s('visLive', ''); const badge = document.getElementById('visBadge'); const badgeSoon = document.getElementById('visBadgeSoon'); if (badge) badge.style.display = visitors.length ? '' : 'none'; if (badgeSoon) badgeSoon.style.display = visitors.length ? 'none' : ''; window.aiContext.visitors = { total: uniqueCompanies, today: todayCount, people: peopleCount }; }; try { const res = await fetch(`${WORKER}/api/visitors`); const data = await res.json(); const visitors = data.visitors || []; showLive(visitors); } catch(e) { visitorsLoaded = false; // allow retry on next tab click showLive([], '// COULD NOT REACH VISITOR DATA — CHECK WORKER DEPLOYMENT'); } } // ── SIDEBAR COLLAPSE TOGGLE ─────────────────────────────────────────────────── function toggleSbSection(bodyId, chevronId) { const body = document.getElementById(bodyId); const chev = document.getElementById(chevronId); if (!body) return; const collapsed = body.style.display === 'none'; body.style.display = collapsed ? '' : 'none'; if (chev) chev.style.transform = collapsed ? '' : 'rotate(-90deg)'; } function formatRelTime(iso) { if (!iso) return 'recently'; // Already a formatted string (e.g. "2 min ago") — return as-is if (typeof iso === 'string' && /ago|just now/i.test(iso)) return iso; const d = new Date(iso); if (isNaN(d.getTime())) return 'recently'; const diff = Math.floor((Date.now() - d) / 1000); if (diff < 60) return diff + ' sec ago'; if (diff < 3600) return Math.floor(diff/60) + ' min ago'; if (diff < 86400) return Math.floor(diff/3600) + ' hr ago'; return Math.floor(diff/86400) + ' day ago'; } // ── AI RECOMMENDATIONS + HIGHLIGHTS ────────────────────────────────────────── function updateInsights() { const ga = window.aiContext?.ga || {}; const meta = window.aiContext?.meta || {}; const recs = []; if (ga.bounceRate) { const br = parseFloat(ga.bounceRate); if (br > 70) recs.push({ t:'rw', i:'⚠', h:'High Bounce Rate', d:`${ga.bounceRate} bounce rate — visitors are leaving quickly. A/B test your hero headline and make sure your primary CTA is visible above the fold.` }); else if (br < 40) recs.push({ t:'ro', i:'✓', h:'Strong Page Engagement', d:`${ga.bounceRate} bounce rate — excellent. Visitors are exploring multiple pages. Double down on the content bringing them in.` }); } if (ga.conversionRate) { const cr = parseFloat(ga.conversionRate); if (cr < 1) recs.push({ t:'rw', i:'⚠', h:'Low Conversion Rate', d:`${ga.conversionRate} conversion rate. Simplify your form, add trust signals (testimonials, logos), and test your CTA button copy and color.` }); else if (cr >= 3) recs.push({ t:'ro', i:'✓', h:'Healthy Conversion Rate', d:`${ga.conversionRate} conversion rate — strong. Focus on scaling traffic while maintaining this efficiency.` }); } if (ga.newUsersPct && parseInt(ga.newUsersPct) > 90) { recs.push({ t:'ri', i:'ℹ', h:'Almost All New Visitors', d:`${ga.newUsersPct} new visitors. Launch a retargeting campaign or email capture popup to re-engage first-time visitors and build a returning audience.` }); } if (ga.avgDuration) { const secs = parseInt(ga.avgDuration); const isShort = ga.avgDuration.includes('s') && !ga.avgDuration.includes('m') && secs < 45; if (isShort) recs.push({ t:'rw', i:'⚠', h:'Short Session Duration', d:`Average ${ga.avgDuration} on site. Add an engaging video, interactive tool, or stronger opening hook to increase time-on-page.` }); } if (ga.allChannels) { const direct = ga.allChannels.find(c => c.channel.toLowerCase() === 'direct'); if (direct && direct.pct > 55) recs.push({ t:'ri', i:'ℹ', h:'Heavy Direct Traffic', d:`${direct.pct}% Direct traffic means people already know you. Invest in SEO and paid campaigns to bring in net-new audiences.` }); const ai = ga.allChannels.find(c => c.channel.toLowerCase().includes('ai')); if (ai && ai.pct > 1) recs.push({ t:'ro', i:'★', h:'AI Assistants Sending Traffic', d:`${ai.pct}% of sessions come from AI tools (ChatGPT, Perplexity, etc.). Structure your content to be cited by AI — authoritative, direct, well-headlined.` }); } if (meta.ctr) { const ctr = parseFloat(meta.ctr); if (ctr < 1) recs.push({ t:'rw', i:'⚠', h:'Meta CTR Below 1%', d:`${meta.ctr} CTR on Meta ads. Refresh your creative — try video, a bold new headline, or tighten your audience targeting to improve ad relevance.` }); else if (ctr > 2) recs.push({ t:'ro', i:'✓', h:'Strong Meta CTR', d:`${meta.ctr} CTR — above average. Scale budget on this creative while it's hot, and watch frequency to prevent fatigue.` }); } if (meta.freq && parseFloat(meta.freq) > 3.5) { recs.push({ t:'rw', i:'⚠', h:'Ad Fatigue Risk', d:`Meta frequency is ${meta.freq}×. Your audience is seeing the same ad too often. Introduce new creative variants now before CTR drops.` }); } if (ga.devices) { if ((ga.devices.mobile || 0) > 60) recs.push({ t:'ri', i:'📱', h:'Majority Mobile Traffic', d:`${ga.devices.mobile}% mobile visitors. Audit your mobile page speed (target under 3s) and ensure CTAs are thumb-friendly above the fold.` }); if ((ga.devices.desktop || 0) > 90) recs.push({ t:'ri', i:'🖥', h:'Near-100% Desktop', d:`${ga.devices.desktop}% desktop traffic. Verify your landing pages and ads are optimized for widescreen layouts and desktop user intent.` }); } if (ga.conversions !== undefined && parseInt(ga.conversions) === 0 && parseInt(ga.sessions) > 100) { recs.push({ t:'rw', i:'🚨', h:'Zero Conversions Tracked', d:`No conversions recorded with ${Number(ga.sessions).toLocaleString()} sessions. Check GA4 Admin → Events to confirm your conversion events are marked and firing correctly.` }); } if (recs.length === 0 && (ga.sessions || meta.spend)) { recs.push({ t:'ro', i:'✓', h:'Metrics Look Stable', d:'No critical issues detected. Keep monitoring bounce rate, conversion rate, and ad frequency — small changes can signal larger trends early.' }); } const list = document.getElementById('aiRecsList'); if (list && recs.length) { list.innerHTML = recs.slice(0,5).map(r => `
${r.i}
${r.h}
${r.d}
` ).join(''); } // Highlights ticker const items = []; if (ga.sessions) items.push(`SESSIONS ${Number(ga.sessions).toLocaleString()}`); if (ga.bounceRate) items.push(`BOUNCE ${ga.bounceRate}`); if (ga.conversionRate) items.push(`CVR ${ga.conversionRate}`); if (ga.avgDuration) items.push(`AVG SESSION ${ga.avgDuration}`); if (ga.topChannel) items.push(`TOP CHANNEL ${ga.topChannel.toUpperCase()}`); if (ga.newUsersPct) items.push(`NEW VISITORS ${ga.newUsersPct}`); if (meta.spend) items.push(`META SPEND ${meta.spend}`); if (meta.ctr) items.push(`META CTR ${meta.ctr}`); if (ga.devices) { const top = Object.entries(ga.devices).sort((a,b)=>b[1]-a[1])[0]; if (top) items.push(`TOP DEVICE ${top[0].toUpperCase()} ${top[1]}%`); } if (items.length) { const bar = document.getElementById('highlightsBar'); const inner = document.getElementById('hlTickerInner'); if (bar && inner) { const key = items.join('|'); // Only replace innerHTML when content actually changes — prevents animation restart glitch if (inner.dataset.key !== key) { inner.dataset.key = key; inner.innerHTML = [...items,...items].map(h=>`${h}`).join(''); } bar.style.display = 'flex'; } } } function toggleAIRecs() { const panel = document.getElementById('aiRecsPanel'); const chevron = document.getElementById('aiRecsChevron'); if (!panel) return; const open = panel.style.display === 'block'; panel.style.display = open ? 'none' : 'block'; if (chevron) chevron.style.transform = open ? '' : 'rotate(180deg)'; } function updateBottomVisitors(visitors) { const el = document.getElementById('bottomVisList'); if (!el) return; if (!visitors || !visitors.length) { el.innerHTML = '
// NO VISITORS IDENTIFIED YET — RB2B WARMING UP
'; return; } const today = new Date().toDateString(); el.innerHTML = visitors.slice(0, 8).map(v => { const isToday = v.when && new Date(v.when).toDateString() === today; const relTime = v.when ? formatRelTime(v.when) : ''; const pageLbl = v.page ? (v.page === '/' ? 'Homepage' : v.page.split('/').filter(Boolean).pop().replace(/-/g,' ').slice(0,30)) : ''; return `
${v.company || 'Unknown Company'}
${pageLbl}
${relTime}
`; }).join(''); } // ── GOAL TRACKING ────────────────────────────────────────────────────────────── // Monthly targets for N.A.M.E. — update these as goals change const NAME_GOALS = [ { label: 'Monthly Sessions', target: 3000, get: () => window.aiContext?.ga?.sessions, unit: '', color: '#00b4ff' }, { label: 'Conversions', target: 50, get: () => window.aiContext?.ga?.conversions, unit: '', color: '#00ff88' }, { label: 'Meta Spend Target', target: 5000, get: () => { const m=window.aiContext?.meta; return m?.spend ? parseFloat(String(m.spend).replace(/[^0-9.]/g,'')) : null; }, unit: '$', color: '#ff6b00' }, ]; function renderGoals() { const container = document.getElementById('sbGoals'); const block = document.getElementById('sbGoalsBlock'); if (!container || !block) return; let hasData = false; const html = NAME_GOALS.map(g => { const raw = g.get(); const current = raw != null ? parseFloat(String(raw).replace(/[^0-9.]/g,'')) : null; if (current != null && !isNaN(current)) hasData = true; const pct = (current != null && !isNaN(current)) ? Math.min(100, Math.round((current / g.target) * 100)) : 0; const dispCurrent = (current != null && !isNaN(current)) ? (g.unit === '$' ? '$'+Math.round(current).toLocaleString() : Math.round(current).toLocaleString()) : '—'; const dispTarget = g.unit === '$' ? '$'+g.target.toLocaleString() : g.target.toLocaleString(); const pctColor = pct >= 80 ? '#00ff88' : pct >= 50 ? g.color : '#ff6b00'; return `
${g.label}${pct}%
${dispCurrent}${dispTarget}
`; }).join(''); container.innerHTML = html; if (hasData) block.style.display = ''; } // Extend updateInsights to also run goals const _origUpdateInsights = updateInsights; updateInsights = function() { _origUpdateInsights(); renderGoals(); }; // Also run goals when sidebar stats update (Meta data path) const _origUpdateSidebarStats = updateSidebarStats; updateSidebarStats = function() { _origUpdateSidebarStats(); renderGoals(); }; // ── VI — IPINFO.IO CURRENT VIEWER IDENTIFICATION ────────────────────────────── // Identifies the company/org of whoever is viewing the dashboard right now const VI_IPINFO_TOKEN = 'fd144105629551'; async function identifyCurrentViewer() { const coCel = document.getElementById('viViewerCo'); const detCel = document.getElementById('viViewerDetail'); const metCel = document.getElementById('viViewerMeta'); try { const controller = new AbortController(); const tid = setTimeout(() => controller.abort(), 8000); const res = await fetch(`https://ipinfo.io?token=${VI_IPINFO_TOKEN}`, { signal: controller.signal }); clearTimeout(tid); if (!res.ok) throw new Error('ipinfo ' + res.status); const d = await res.json(); // Strip the ASN prefix from org field (e.g. "AS7922 Comcast" → "Comcast") const orgName = d.org ? d.org.replace(/^AS\d+\s+/i, '') : (d.hostname || 'Unknown Network'); const location = [d.city, d.region, d.country].filter(Boolean).join(', '); const tz = d.timezone ? ' · ' + d.timezone : ''; if (coCel) { coCel.textContent = orgName; coCel.style.color = '#fff'; } if (detCel) { detCel.textContent = location || 'Location unavailable'; detCel.style = ''; } if (metCel) metCel.textContent = `IP ${d.ip || '—'}${tz}`; window.aiContext.currentViewer = { company: orgName, ip: d.ip, location, org: d.org }; } catch(e) { // Show fallback even if lookup fails if (coCel) coCel.textContent = 'Network lookup unavailable'; if (detCel) detCel.textContent = 'Check your connection and reload'; if (metCel) metCel.textContent = 'ipinfo.io · token active'; console.warn('VI ipinfo:', e.message); } } // Run ipinfo lookup on page load; re-run when VI tab is opened setTimeout(identifyCurrentViewer, 800); const _origLoadVisitors = loadVisitors; loadVisitors = function() { identifyCurrentViewer(); return _origLoadVisitors(); }; // ── MOBILE NAVIGATION ───────────────────────────────────────────────────────── function setMobNav(key) { document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById('mn-' + key); if (btn) btn.classList.add('active'); } // ── CLIENT CAMPAIGNS / CLIENT MANAGEMENT ────────────────────────────────────── let campLoaded = false; let campClients = []; let campCurrentClient = null; let cdTrendChart = null; const PIXEL_BASE = 'https://name-analytics-worker.gentle-block-c949.workers.dev'; async function loadCampaigns() { if (campLoaded) return; campLoaded = true; try { const res = await fetch(`${WORKER}/api/clients`); const data = await res.json(); if (data.setup || data.error) { document.getElementById('campKvSetup').style.display = ''; renderClientGrid([]); return; } campClients = data.clients || []; renderClientGrid(campClients); } catch(e) { document.getElementById('campKvSetup').style.display = ''; renderClientGrid([]); } } function renderClientGrid(clients) { const grid = document.getElementById('campClientGrid'); if (!clients.length) { grid.innerHTML = `
// NO CLIENTS YET — CLICK "+ ADD CLIENT" TO ONBOARD YOUR FIRST CLIENT
`; return; } grid.innerHTML = clients.map(c => `
${c.name}
${c.industry || ''}${c.website ? ' · ' + c.website.replace('https://','').replace('http://','') : ''}
${c.pixelId ? '⬡ PIXEL ACTIVE' : ''} ${c.googleAdsCid ? 'GOOGLE ADS' : ''} ${c.metaAccountId ? 'META' : ''}
PAGE VIEWS
VISITORS
FORM FILLS
ADDED ${new Date(c.createdAt||Date.now()).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase()}
`).join(''); // Load quick stats for each client card clients.forEach(c => loadClientCardStats(c)); } async function loadClientCardStats(client) { try { const res = await fetch(`${WORKER}/api/clients/${client.id}/stats`); const data = await res.json(); const el = document.getElementById(`cstats-${client.id}`); if (!el || !data.summary) return; const s = data.summary; el.innerHTML = `
${(s.pageviews||0).toLocaleString()}
PAGE VIEWS
${(s.unique_visitors||0).toLocaleString()}
VISITORS
${(s.form_fills||0).toLocaleString()}
FORM FILLS
`; } catch(e) {} } function openClientDetail(clientId) { campCurrentClient = campClients.find(c => c.id === clientId); if (!campCurrentClient) return; document.getElementById('campClientList').style.display = 'none'; document.getElementById('campClientDetail').style.display = ''; document.getElementById('cdClientName').textContent = campCurrentClient.name; document.getElementById('cdClientMeta').textContent = [campCurrentClient.industry, campCurrentClient.website].filter(Boolean).join(' · '); // Pixel snippet const snippet = `